浏览代码

Merge pull request #2445 from MediaBrowser/beta

Beta
Luke 8 年之前
父节点
当前提交
2ceaa50ea7
共有 100 个文件被更改,包括 2874 次插入5143 次删除
  1. 10 2
      Emby.Common.Implementations/Net/UdpSocket.cs
  2. 0 5
      Emby.Dlna/Didl/DidlBuilder.cs
  3. 9 5
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  4. 35 35
      Emby.Server.Core/ApplicationHost.cs
  5. 0 12
      Emby.Server.Core/Configuration/ServerConfigurationManager.cs
  6. 13 8
      Emby.Server.Core/IO/LibraryMonitor.cs
  7. 1 1
      Emby.Server.Implementations/Connect/ConnectManager.cs
  8. 0 13
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  9. 0 5
      Emby.Server.Implementations/Dto/DtoService.cs
  10. 2 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  11. 13 18
      Emby.Server.Implementations/Library/LibraryManager.cs
  12. 11 10
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  13. 6 0
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  14. 1 1
      Emby.Server.Implementations/Library/SearchEngine.cs
  15. 130 35
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  16. 4 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  17. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
  18. 84 202
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  19. 28 27
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  20. 18 16
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  21. 41 13
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  22. 24 13
      Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs
  23. 3 13
      Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs
  24. 1 1
      Emby.Server.Implementations/Migrations/GuideMigration.cs
  25. 7 0
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  26. 2 1
      Emby.Server.Implementations/Udp/UdpServer.cs
  27. 1 1
      Emby.Server.Implementations/packages.config
  28. 7 22
      MediaBrowser.Api/BasePeriodicWebSocketListener.cs
  29. 1 1
      MediaBrowser.Api/Images/RemoteImageService.cs
  30. 0 2
      MediaBrowser.Api/ItemUpdateService.cs
  31. 4 4
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  32. 4 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  33. 45 1010
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  34. 8 6
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  35. 15 12
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  36. 8 7
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  37. 5 3
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  38. 39 12
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  39. 4 149
      MediaBrowser.Api/Playback/StreamRequest.cs
  40. 17 91
      MediaBrowser.Api/Playback/StreamState.cs
  41. 0 14
      MediaBrowser.Api/SearchService.cs
  42. 1 2
      MediaBrowser.Api/StartupWizardService.cs
  43. 5 1
      MediaBrowser.Api/UserLibrary/PlaystateService.cs
  44. 0 6
      MediaBrowser.Controller/Entities/BaseItem.cs
  45. 0 1
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  46. 2 4
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  47. 1 1
      MediaBrowser.Controller/Library/ILibraryManager.cs
  48. 4 0
      MediaBrowser.Controller/LiveTv/ChannelInfo.cs
  49. 1 2
      MediaBrowser.Controller/LiveTv/IListingsProvider.cs
  50. 0 1
      MediaBrowser.Controller/LiveTv/TimerInfo.cs
  51. 2 2
      MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
  52. 159 39
      MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
  53. 1 2
      MediaBrowser.Controller/Providers/IProviderManager.cs
  54. 0 12
      MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
  55. 0 5
      MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
  56. 5 3
      MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
  57. 7 791
      MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
  58. 1681 0
      MediaBrowser.MediaEncoding/Encoder/EncodingHelper.cs
  59. 10 73
      MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
  60. 12 606
      MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
  61. 118 0
      MediaBrowser.MediaEncoding/Encoder/EncodingJobInfo.cs
  62. 16 10
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  63. 12 11
      MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs
  64. 2 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  65. 1 8
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  66. 0 8
      MediaBrowser.Model/Configuration/PathSubstitution.cs
  67. 2 6
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  68. 5 0
      MediaBrowser.Model/Dlna/ITranscoderSupport.cs
  69. 58 6
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  70. 9 1
      MediaBrowser.Model/Dlna/StreamInfo.cs
  71. 0 6
      MediaBrowser.Model/Dto/BaseItemDto.cs
  72. 7 5
      MediaBrowser.Model/Entities/MediaStream.cs
  73. 0 12
      MediaBrowser.Model/LiveTv/LiveTvOptions.cs
  74. 0 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  75. 0 5
      MediaBrowser.Model/MediaInfo/MediaInfo.cs
  76. 2 1
      MediaBrowser.Model/Net/IUdpSocket.cs
  77. 0 5
      MediaBrowser.Model/Querying/ItemFields.cs
  78. 2 0
      MediaBrowser.Model/Session/PlaybackStopInfo.cs
  79. 7 3
      MediaBrowser.Providers/Manager/ImageSaver.cs
  80. 1 1
      MediaBrowser.Providers/Manager/MetadataService.cs
  81. 1 2
      MediaBrowser.Providers/Manager/ProviderManager.cs
  82. 0 9
      MediaBrowser.Providers/Manager/ProviderUtils.cs
  83. 0 5
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  84. 1 1
      MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
  85. 6 1
      MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs
  86. 1 8
      MediaBrowser.Providers/Omdb/OmdbItemProvider.cs
  87. 15 18
      MediaBrowser.Providers/Omdb/OmdbProvider.cs
  88. 6 1
      MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs
  89. 16 122
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  90. 15 59
      MediaBrowser.WebDashboard/Api/PackageCreator.cs
  91. 1 1429
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  92. 0 11
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  93. 7 7
      MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
  94. 6 6
      MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
  95. 20 5
      MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
  96. 16 16
      MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
  97. 8 8
      MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
  98. 6 4
      MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
  99. 12 12
      MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
  100. 3 5
      RSSDP/ISsdpCommunicationsServer.cs

+ 10 - 2
Emby.Common.Implementations/Net/UdpSocket.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Net;
 using System.Net.Sockets;
 using System.Security;
+using System.Threading;
 using System.Threading.Tasks;
 using Emby.Common.Implementations.Networking;
 using MediaBrowser.Model.Net;
@@ -56,7 +57,7 @@ namespace Emby.Common.Implementations.Net
             state.TaskCompletionSource = tcs;
 
 #if NETSTANDARD1_6
-            _Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer),SocketFlags.None, state.RemoteEndPoint)
+            _Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer), SocketFlags.None, state.RemoteEndPoint)
                 .ContinueWith((task, asyncState) =>
                 {
                     if (task.Status != TaskStatus.Faulted)
@@ -73,7 +74,7 @@ namespace Emby.Common.Implementations.Net
             return tcs.Task;
         }
 
-        public Task SendAsync(byte[] buffer, int size, IpEndPointInfo endPoint)
+        public Task SendAsync(byte[] buffer, int size, IpEndPointInfo endPoint, CancellationToken cancellationToken)
         {
             ThrowIfDisposed();
 
@@ -91,6 +92,8 @@ namespace Emby.Common.Implementations.Net
                 buffer = copy;
             }
 
+            cancellationToken.ThrowIfCancellationRequested();
+
             _Socket.SendTo(buffer, ipEndPoint);
             return Task.FromResult(true);
 #else
@@ -100,6 +103,11 @@ namespace Emby.Common.Implementations.Net
             {
                 _Socket.BeginSendTo(buffer, 0, size, SocketFlags.None, ipEndPoint, result =>
                 {
+                    if (cancellationToken.IsCancellationRequested)
+                    {
+                        taskSource.TrySetCanceled();
+                        return;
+                    }
                     try
                     {
                         _Socket.EndSend(result);

+ 0 - 5
Emby.Dlna/Didl/DidlBuilder.cs

@@ -646,11 +646,6 @@ namespace Emby.Dlna.Didl
             {
                 var desc = item.Overview;
 
-                if (!string.IsNullOrEmpty(item.ShortOverview))
-                {
-                    desc = item.ShortOverview;
-                }
-
                 if (!string.IsNullOrWhiteSpace(desc))
                 {
                     AddValue(writer, "dc", "description", desc, NS_DC);

+ 9 - 5
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -75,16 +75,20 @@ namespace Emby.Dlna.Ssdp
                         // Enable listening for notifications (optional)
                         _deviceLocator.StartListeningForNotifications();
 
-                        await _deviceLocator.SearchAsync().ConfigureAwait(false);
+                        await _deviceLocator.SearchAsync(_tokenSource.Token).ConfigureAwait(false);
+
+                        var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000;
+
+                        await Task.Delay(delay, _tokenSource.Token).ConfigureAwait(false);
+                    }
+                    catch (OperationCanceledException)
+                    {
+
                     }
                     catch (Exception ex)
                     {
                         _logger.ErrorException("Error searching for devices", ex);
                     }
-
-                    var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000;
-
-                    await Task.Delay(delay, _tokenSource.Token).ConfigureAwait(false);
                 }
 
             }, CancellationToken.None, TaskCreationOptions.LongRunning);

+ 35 - 35
Emby.Server.Core/ApplicationHost.cs

@@ -413,41 +413,41 @@ namespace Emby.Server.Core
 
             var result = new JsonSerializer(FileSystemManager, LogManager.GetLogger("JsonSerializer"));
 
-            ServiceStack.Text.JsConfig<LiveTvProgram>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<LiveTvChannel>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<LiveTvVideoRecording>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<LiveTvAudioRecording>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Series>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Audio>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<MusicAlbum>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<MusicArtist>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<MusicGenre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<MusicVideo>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Movie>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Playlist>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<AudioPodcast>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<AudioBook>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Trailer>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<BoxSet>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Episode>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Season>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Book>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<CollectionFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Folder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Game>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<GameGenre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<GameSystem>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Genre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Person>.ExcludePropertyNames = new[] { "PlaceOfBirth", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Photo>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<PhotoAlbum>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Studio>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<UserRootFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<UserView>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Video>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Year>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<Channel>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
-            ServiceStack.Text.JsConfig<AggregateFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<LiveTvProgram>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<LiveTvChannel>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<LiveTvVideoRecording>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<LiveTvAudioRecording>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Series>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Audio>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<MusicAlbum>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<MusicArtist>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<MusicGenre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<MusicVideo>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Movie>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Playlist>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<AudioPodcast>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<AudioBook>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Trailer>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<BoxSet>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Episode>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Season>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Book>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<CollectionFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Folder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Game>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<GameGenre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<GameSystem>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Genre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Person>.ExcludePropertyNames = new[] { "PlaceOfBirth", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Photo>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<PhotoAlbum>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Studio>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<UserRootFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<UserView>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Video>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Year>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<Channel>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
+            ServiceStack.Text.JsConfig<AggregateFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "Taglines", "Keywords", "ExtraType" };
 
             return result;
         }

+ 0 - 12
Emby.Server.Core/Configuration/ServerConfigurationManager.cs

@@ -141,7 +141,6 @@ namespace Emby.Server.Core.Configuration
         {
             var newConfig = (ServerConfiguration)newConfiguration;
 
-            ValidatePathSubstitutions(newConfig);
             ValidateMetadataPath(newConfig);
             ValidateSslCertificate(newConfig);
 
@@ -173,17 +172,6 @@ namespace Emby.Server.Core.Configuration
             }
         }
 
-        private void ValidatePathSubstitutions(ServerConfiguration newConfig)
-        {
-            foreach (var map in newConfig.PathSubstitutions)
-            {
-                if (string.IsNullOrWhiteSpace(map.From) || string.IsNullOrWhiteSpace(map.To))
-                {
-                    throw new ArgumentException("Invalid path substitution");
-                }
-            }
-        }
-
         /// <summary>
         /// Validates the metadata path.
         /// </summary>

+ 13 - 8
Emby.Server.Core/IO/LibraryMonitor.cs

@@ -283,18 +283,24 @@ namespace Emby.Server.Core.IO
         /// <param name="path">The path.</param>
         private void StartWatchingPath(string path)
         {
+            if (!_fileSystem.DirectoryExists(path))
+            {
+                // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
+                Logger.Info("Skipping realtime monitor for {0} because the path does not exist", path);
+                return;
+            }
+
+            // Already being watched
+            if (_fileSystemWatchers.ContainsKey(path))
+            {
+                return;
+            }
+
             // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel
             Task.Run(() =>
             {
                 try
                 {
-                    if (!_fileSystem.DirectoryExists(path))
-                    {
-                        // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
-                        Logger.Info("Skipping realtime monitor for {0} because the path does not exist", path);
-                        return;
-                    }
-
                     var newWatcher = new FileSystemWatcher(path, "*")
                     {
                         IncludeSubdirectories = true
@@ -326,7 +332,6 @@ namespace Emby.Server.Core.IO
                     }
                     else
                     {
-                        Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary.", path);
                         newWatcher.Dispose();
                     }
 

+ 1 - 1
Emby.Server.Implementations/Connect/ConnectManager.cs

@@ -995,7 +995,7 @@ namespace Emby.Server.Implementations.Connect
 
                         if (changed)
                         {
-                            await _providerManager.SaveImage(user, imageUrl, null, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false);
+                            await _providerManager.SaveImage(user, imageUrl, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false);
 
                             await user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem)
                             {

+ 0 - 13
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -257,7 +257,6 @@ namespace Emby.Server.Implementations.Data
                     AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
                     AddColumn(db, "TypedBaseItems", "SeriesSortName", "Text", existingColumnNames);
                     AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
-                    AddColumn(db, "TypedBaseItems", "ShortOverview", "Text", existingColumnNames);
                     AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
                     AddColumn(db, "TypedBaseItems", "Keywords", "Text", existingColumnNames);
                     AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
@@ -466,7 +465,6 @@ namespace Emby.Server.Implementations.Data
             "InheritedParentalRatingValue",
             "InheritedTags",
             "ExternalSeriesId",
-            "ShortOverview",
             "Tagline",
             "Keywords",
             "ProviderIds",
@@ -598,7 +596,6 @@ namespace Emby.Server.Implementations.Data
                 "SeriesId",
                 "SeriesSortName",
                 "ExternalSeriesId",
-                "ShortOverview",
                 "Tagline",
                 "Keywords",
                 "ProviderIds",
@@ -1038,7 +1035,6 @@ namespace Emby.Server.Implementations.Data
             }
 
             saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
-            saveItemStatement.TryBind("@ShortOverview", item.ShortOverview);
             saveItemStatement.TryBind("@Tagline", item.Tagline);
 
             if (item.Keywords.Count > 0)
@@ -1893,15 +1889,6 @@ namespace Emby.Server.Implementations.Data
             }
             index++;
 
-            if (query.HasField(ItemFields.ShortOverview))
-            {
-                if (!reader.IsDBNull(index))
-                {
-                    item.ShortOverview = reader.GetString(index);
-                }
-                index++;
-            }
-
             if (query.HasField(ItemFields.Taglines))
             {
                 if (!reader.IsDBNull(index))

+ 0 - 5
Emby.Server.Implementations/Dto/DtoService.cs

@@ -1052,11 +1052,6 @@ namespace Emby.Server.Implementations.Dto
                 dto.OriginalTitle = item.OriginalTitle;
             }
 
-            if (fields.Contains(ItemFields.ShortOverview))
-            {
-                dto.ShortOverview = item.ShortOverview;
-            }
-
             if (fields.Contains(ItemFields.ParentId))
             {
                 var displayParentId = item.DisplayParentId;

+ 2 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -309,8 +309,8 @@
       <Project>{4f26d5d8-a7b0-42b3-ba42-7cb7d245934e}</Project>
       <Name>SocketHttpListener.Portable</Name>
     </ProjectReference>
-    <Reference Include="Emby.XmlTv, Version=1.0.6193.39741, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\Emby.XmlTv.1.0.3\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
+    <Reference Include="Emby.XmlTv, Version=1.0.6241.4924, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\Emby.XmlTv.1.0.5\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
       <Private>True</Private>
     </Reference>
     <Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">

+ 13 - 18
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1927,11 +1927,18 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.RetrieveItem(id);
         }
 
-        public IEnumerable<Folder> GetCollectionFolders(BaseItem item)
+        public List<Folder> GetCollectionFolders(BaseItem item)
         {
-            while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null)
+            while (item != null)
             {
-                item = item.GetParent();
+                var parent = item.GetParent();
+
+                if (parent == null || parent is AggregateFolder)
+                {
+                    break;
+                }
+
+                item = parent;
             }
 
             if (item == null)
@@ -1941,7 +1948,8 @@ namespace Emby.Server.Implementations.Library
 
             return GetUserRootFolder().Children
                 .OfType<Folder>()
-                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase));
+                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
+                .ToList();
         }
 
         public LibraryOptions GetLibraryOptions(BaseItem item)
@@ -2619,18 +2627,6 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
-            {
-                if (!string.IsNullOrWhiteSpace(map.From))
-                {
-                    var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
-                    if (substitutionResult.Item2)
-                    {
-                        return substitutionResult.Item1;
-                    }
-                }
-            }
-
             return path;
         }
 
@@ -2764,7 +2760,6 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.UpdatePeople(item.Id, people);
         }
 
-        private readonly SemaphoreSlim _dynamicImageResourcePool = new SemaphoreSlim(1, 1);
         public async Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex)
         {
             foreach (var url in image.Path.Split('|'))
@@ -2773,7 +2768,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     _logger.Debug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 
-                    await _providerManagerFactory().SaveImage(item, url, _dynamicImageResourcePool, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
+                    await _providerManagerFactory().SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
 
                     var newImage = item.GetImageInfo(image.Type, imageIndex);
 

+ 11 - 10
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -74,20 +74,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return new MusicArtist();
             }
 
-            if (_config.Configuration.EnableSimpleArtistDetection)
-            {
-                return null;
-            }
+            return null;
+            //if (_config.Configuration.EnableSimpleArtistDetection)
+            //{
+            //    return null;
+            //}
 
-            // Avoid mis-identifying top folders
-            if (args.Parent.IsRoot) return null;
+            //// Avoid mis-identifying top folders
+            //if (args.Parent.IsRoot) return null;
 
-            var directoryService = args.DirectoryService;
+            //var directoryService = args.DirectoryService;
 
-            var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
+            //var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
 
-            // If we contain an album assume we are an artist folder
-            return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
+            //// If we contain an album assume we are an artist folder
+            //return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
         }
 
     }

+ 6 - 0
Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -64,6 +64,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                         episode.SeasonId = season.Id;
                         episode.SeasonName = season.Name;
                     }
+
+                    // Assume season 1 if there's no season folder and a season number could not be determined
+                    if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
+                    {
+                        episode.ParentIndexNumber = 1;
+                    }
                 }
 
                 return episode;

+ 1 - 1
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Library
                 return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2);
             }));
 
-            var returnValue = hints.Where(i => i.Item3 >= 0).OrderBy(i => i.Item3).Select(i => new SearchHintInfo
+            var returnValue = hints.Where(i => i.Item3 >= 0).OrderBy(i => i.Item3).ThenBy(i => i.Item1.SortName).Select(i => new SearchHintInfo
             {
                 Item = i.Item1,
                 MatchedTerm = i.Item2

+ 130 - 35
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                     try
                     {
-                        await provider.Item1.AddMetadata(provider.Item2, enabledChannels, cancellationToken).ConfigureAwait(false);
+                        await AddMetadata(provider.Item1, provider.Item2, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false);
                     }
                     catch (NotSupportedException)
                     {
@@ -409,6 +409,120 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return list;
         }
 
+        private async Task AddMetadata(IListingsProvider provider, ListingsProviderInfo info, List<ChannelInfo> tunerChannels, bool enableCache, CancellationToken cancellationToken)
+        {
+            var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
+
+            foreach (var tunerChannel in tunerChannels)
+            {
+                var epgChannel = GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
+
+                if (epgChannel != null)
+                {
+                    if (!string.IsNullOrWhiteSpace(epgChannel.Name))
+                    {
+                        tunerChannel.Name = epgChannel.Name;
+                    }
+                }
+            }
+        }
+
+        private readonly ConcurrentDictionary<string, List<ChannelInfo>> _epgChannels =
+            new ConcurrentDictionary<string, List<ChannelInfo>>(StringComparer.OrdinalIgnoreCase);
+
+        private async Task<List<ChannelInfo>> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken)
+        {
+            List<ChannelInfo> result;
+            if (!enableCache || !_epgChannels.TryGetValue(info.Id, out result))
+            {
+                result = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
+
+                _epgChannels.AddOrUpdate(info.Id, result, (k, v) => result);
+            }
+
+            return result;
+        }
+
+        private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken)
+        {
+            var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false);
+
+            return GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
+        }
+
+        private string GetMappedChannel(string channelId, List<NameValuePair> mappings)
+        {
+            foreach (NameValuePair mapping in mappings)
+            {
+                if (StringHelper.EqualsIgnoreCase(mapping.Name, channelId))
+                {
+                    return mapping.Value;
+                }
+            }
+            return channelId;
+        }
+
+        private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
+        {
+            return GetEpgChannelFromTunerChannel(info.ChannelMappings.ToList(), tunerChannel, epgChannels);
+        }
+
+        public ChannelInfo GetEpgChannelFromTunerChannel(List<NameValuePair> mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
+        {
+            if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
+            {
+                var tunerChannelId = GetMappedChannel(tunerChannel.TunerChannelId, mappings);
+
+                if (string.IsNullOrWhiteSpace(tunerChannelId))
+                {
+                    tunerChannelId = tunerChannel.TunerChannelId;
+                }
+
+                var channel = epgChannels.FirstOrDefault(i => string.Equals(tunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
+
+                if (channel != null)
+                {
+                    return channel;
+                }
+            }
+
+            if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
+            {
+                var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
+
+                if (string.IsNullOrWhiteSpace(tunerChannelNumber))
+                {
+                    tunerChannelNumber = tunerChannel.Number;
+                }
+
+                var channel = epgChannels.FirstOrDefault(i => string.Equals(tunerChannelNumber, i.Number, StringComparison.OrdinalIgnoreCase));
+
+                if (channel != null)
+                {
+                    return channel;
+                }
+            }
+
+            if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
+            {
+                var normalizedName = NormalizeName(tunerChannel.Name);
+
+                var channel = epgChannels.FirstOrDefault(i => string.Equals(normalizedName, NormalizeName(i.Name ?? string.Empty), StringComparison.OrdinalIgnoreCase));
+
+                if (channel != null)
+                {
+                    return channel;
+                }
+            }
+
+            return null;
+        }
+
+        private string NormalizeName(string value)
+        {
+            return value.Replace(" ", string.Empty).Replace("-", string.Empty);
+        }
+
         public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
         {
             var list = new List<ChannelInfo>();
@@ -663,7 +777,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             existingTimer.ProductionYear = updatedTimer.ProductionYear;
             existingTimer.ProgramId = updatedTimer.ProgramId;
             existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
-            existingTimer.ShortOverview = updatedTimer.ShortOverview;
             existingTimer.StartDate = updatedTimer.StartDate;
             existingTimer.ShowId = updatedTimer.ShowId;
         }
@@ -846,49 +959,37 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
 
-                var channelMappings = GetChannelMappings(provider.Item2);
-                var channelNumber = channel.Number;
-                string mappedChannelNumber;
-                if (channelMappings.TryGetValue(channelNumber, out mappedChannelNumber))
-                {
-                    _logger.Debug("Found mapped channel on provider {0}. Tuner channel number: {1}, Mapped channel number: {2}", provider.Item1.Name, channelNumber, mappedChannelNumber);
-                    channelNumber = mappedChannelNumber;
-                }
+                var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
 
-                var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channelNumber, channel.Name, startDateUtc, endDateUtc, cancellationToken)
-                        .ConfigureAwait(false);
+                List<ProgramInfo> programs;
 
-                var list = programs.ToList();
+                if (epgChannel == null)
+                {
+                    programs = new List<ProgramInfo>();
+                }
+                else
+                {
+                    programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
+                           .ConfigureAwait(false)).ToList();
+                }
 
                 // Replace the value that came from the provider with a normalized value
-                foreach (var program in list)
+                foreach (var program in programs)
                 {
                     program.ChannelId = channelId;
                 }
 
-                if (list.Count > 0)
+                if (programs.Count > 0)
                 {
-                    SaveEpgDataForChannel(channelId, list);
+                    SaveEpgDataForChannel(channelId, programs);
 
-                    return list;
+                    return programs;
                 }
             }
 
             return new List<ProgramInfo>();
         }
 
-        private Dictionary<string, string> GetChannelMappings(ListingsProviderInfo info)
-        {
-            var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            foreach (var mapping in info.ChannelMappings)
-            {
-                dict[mapping.Name] = mapping.Value;
-            }
-
-            return dict;
-        }
-
         private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
         {
             return GetConfiguration().ListingProviders
@@ -1755,7 +1856,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     {
                         Name = timer.Name,
                         HomePageUrl = timer.HomePageUrl,
-                        ShortOverview = timer.ShortOverview,
                         Overview = timer.Overview,
                         Genres = timer.Genres,
                         CommunityRating = timer.CommunityRating,
@@ -1959,11 +2059,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                         writer.WriteElementString("genre", genre);
                     }
 
-                    if (!string.IsNullOrWhiteSpace(item.ShortOverview))
-                    {
-                        writer.WriteElementString("outline", item.ShortOverview);
-                    }
-
                     if (!string.IsNullOrWhiteSpace(item.HomePageUrl))
                     {
                         writer.WriteElementString("website", item.HomePageUrl);

+ 4 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -154,7 +154,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
             var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
-            var commandLineArgs = "-i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+            var mapArgs = string.Equals(OutputFormat, "mkv", StringComparison.OrdinalIgnoreCase) ? "-map 0" : "-sn";
+            // temporary
+            mapArgs = "-sn";
+            var commandLineArgs = "-i \"{0}\"{4} " + mapArgs + " {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
 
             long startTimeTicks = 0;
             //if (mediaSource.DateLiveStreamOpened.HasValue)

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs

@@ -61,7 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             timerInfo.HomePageUrl = programInfo.HomePageUrl;
             timerInfo.CommunityRating = programInfo.CommunityRating;
             timerInfo.Overview = programInfo.Overview;
-            timerInfo.ShortOverview = programInfo.ShortOverview;
             timerInfo.OfficialRating = programInfo.OfficialRating;
             timerInfo.IsRepeat = programInfo.IsRepeat;
             timerInfo.SeriesId = programInfo.SeriesId;

+ 84 - 202
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -15,6 +15,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Extensions;
 
 namespace Emby.Server.Implementations.LiveTv.Listings
 {
@@ -60,8 +61,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return dates;
         }
 
-        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
         {
+            if (string.IsNullOrWhiteSpace(channelId))
+            {
+                throw new ArgumentNullException("channelId");
+            }
+
+            // Normalize incoming input
+            channelId = channelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
+
             List<ProgramInfo> programsInfo = new List<ProgramInfo>();
 
             var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
@@ -80,15 +89,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
 
-            ScheduleDirect.Station station = GetStation(info.ListingsId, channelNumber, channelName);
-
-            if (station == null)
-            {
-                _logger.Info("No Schedules Direct Station found for channel {0} with name {1}", channelNumber, channelName);
-                return programsInfo;
-            }
-
-            string stationID = station.stationID;
+            string stationID = channelId;
 
             _logger.Info("Channel Station ID is: " + stationID);
             List<ScheduleDirect.RequestScheduleForChannel> requestList =
@@ -122,7 +123,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 StreamReader reader = new StreamReader(response.Content);
                 string responseString = reader.ReadToEnd();
                 var dailySchedules = _jsonSerializer.DeserializeFromString<List<ScheduleDirect.Day>>(responseString);
-                _logger.Debug("Found " + dailySchedules.Count + " programs on " + channelNumber + " ScheduleDirect");
+                _logger.Debug("Found " + dailySchedules.Count + " programs on " + stationID + " ScheduleDirect");
 
                 httpOptions = new HttpRequestOptions()
                 {
@@ -180,7 +181,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                             }
                         }
 
-                        programsInfo.Add(GetProgram(channelNumber, schedule, programDict[schedule.programID]));
+                        programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
                     }
                 }
             }
@@ -202,183 +203,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return 0;
         }
 
-        private readonly object _channelCacheLock = new object();
-        private ScheduleDirect.Station GetStation(string listingsId, string channelNumber, string channelName)
+        private string GetChannelNumber(ScheduleDirect.Map map)
         {
-            lock (_channelCacheLock)
-            {
-                Dictionary<string, ScheduleDirect.Station> channelPair;
-                if (_channelPairingCache.TryGetValue(listingsId, out channelPair))
-                {
-                    ScheduleDirect.Station station;
-
-                    if (!string.IsNullOrWhiteSpace(channelNumber) && channelPair.TryGetValue(channelNumber, out station))
-                    {
-                        return station;
-                    }
-
-                    if (!string.IsNullOrWhiteSpace(channelName))
-                    {
-                        channelName = NormalizeName(channelName);
-
-                        var result = channelPair.Values.FirstOrDefault(i => string.Equals(NormalizeName(i.callsign ?? string.Empty), channelName, StringComparison.OrdinalIgnoreCase));
-
-                        if (result != null)
-                        {
-                            return result;
-                        }
-                    }
+            var channelNumber = map.logicalChannelNumber;
 
-                    if (!string.IsNullOrWhiteSpace(channelNumber))
-                    {
-                        return channelPair.Values.FirstOrDefault(i => string.Equals(NormalizeName(i.stationID ?? string.Empty), channelNumber, StringComparison.OrdinalIgnoreCase));
-                    }
-                }
-
-                return null;
-            }
-        }
-
-        private void AddToChannelPairCache(string listingsId, string channelNumber, ScheduleDirect.Station schChannel)
-        {
-            lock (_channelCacheLock)
-            {
-                Dictionary<string, ScheduleDirect.Station> cache;
-                if (_channelPairingCache.TryGetValue(listingsId, out cache))
-                {
-                    cache[channelNumber] = schChannel;
-                }
-                else
-                {
-                    cache = new Dictionary<string, ScheduleDirect.Station>();
-                    cache[channelNumber] = schChannel;
-                    _channelPairingCache[listingsId] = cache;
-                }
-            }
-        }
-
-        private void ClearPairCache(string listingsId)
-        {
-            lock (_channelCacheLock)
+            if (string.IsNullOrWhiteSpace(channelNumber))
             {
-                Dictionary<string, ScheduleDirect.Station> cache;
-                if (_channelPairingCache.TryGetValue(listingsId, out cache))
-                {
-                    cache.Clear();
-                }
+                channelNumber = map.channel;
             }
-        }
-
-        private int GetChannelPairCacheCount(string listingsId)
-        {
-            lock (_channelCacheLock)
+            if (string.IsNullOrWhiteSpace(channelNumber))
             {
-                Dictionary<string, ScheduleDirect.Station> cache;
-                if (_channelPairingCache.TryGetValue(listingsId, out cache))
-                {
-                    return cache.Count;
-                }
-
-                return 0;
+                channelNumber = map.atscMajor + "." + map.atscMinor;
             }
-        }
+            channelNumber = channelNumber.TrimStart('0');
 
-        private string NormalizeName(string value)
-        {
-            return value.Replace(" ", string.Empty).Replace("-", string.Empty);
+            return channelNumber;
         }
 
-        public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels,
-            CancellationToken cancellationToken)
-        {
-            var listingsId = info.ListingsId;
-            if (string.IsNullOrWhiteSpace(listingsId))
-            {
-                throw new Exception("ListingsId required");
-            }
-
-            var token = await GetToken(info, cancellationToken);
-
-            if (string.IsNullOrWhiteSpace(token))
-            {
-                throw new Exception("token required");
-            }
-
-            ClearPairCache(listingsId);
-
-            var httpOptions = new HttpRequestOptions()
-            {
-                Url = ApiUrl + "/lineups/" + listingsId,
-                UserAgent = UserAgent,
-                CancellationToken = cancellationToken,
-                LogErrorResponseBody = true,
-                // The data can be large so give it some extra time
-                TimeoutMs = 60000
-            };
-
-            httpOptions.RequestHeaders["token"] = token;
-
-            using (var response = await Get(httpOptions, true, info).ConfigureAwait(false))
-            {
-                var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Channel>(response);
-
-                foreach (ScheduleDirect.Map map in root.map)
-                {
-                    var channelNumber = map.logicalChannelNumber;
-
-                    if (string.IsNullOrWhiteSpace(channelNumber))
-                    {
-                        channelNumber = map.channel;
-                    }
-                    if (string.IsNullOrWhiteSpace(channelNumber))
-                    {
-                        channelNumber = map.atscMajor + "." + map.atscMinor;
-                    }
-                    channelNumber = channelNumber.TrimStart('0');
-
-                    _logger.Debug("Found channel: " + channelNumber + " in Schedules Direct");
-
-                    var schChannel = (root.stations ?? new List<ScheduleDirect.Station>()).FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
-                    if (schChannel != null)
-                    {
-                        AddToChannelPairCache(listingsId, channelNumber, schChannel);
-                    }
-                    else
-                    {
-                        AddToChannelPairCache(listingsId, channelNumber, new ScheduleDirect.Station
-                        {
-                            stationID = map.stationID
-                        });
-                    }
-                }
-
-                foreach (ChannelInfo channel in channels)
-                {
-                    var station = GetStation(listingsId, channel.Number, channel.Name);
-
-                    if (station != null)
-                    {
-                        if (station.logo != null)
-                        {
-                            channel.ImageUrl = station.logo.URL;
-                            channel.HasImage = true;
-                        }
-
-                        if (!string.IsNullOrWhiteSpace(station.name))
-                        {
-                            channel.Name = station.name;
-                        }
-                    }
-                    else
-                    {
-                        _logger.Info("Schedules Direct doesnt have data for channel: " + channel.Number + " " + channel.Name);
-                    }
-                }
-            }
-        }
-
-        private ProgramInfo GetProgram(string channel, ScheduleDirect.Program programInfo,
-            ScheduleDirect.ProgramDetails details)
+        private ProgramInfo GetProgram(string channelId, ScheduleDirect.Program programInfo, ScheduleDirect.ProgramDetails details)
         {
             //_logger.Debug("Show type is: " + (details.showType ?? "No ShowType"));
             DateTime startAt = GetDate(programInfo.airDateTime);
@@ -386,7 +228,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             ProgramAudio audioType = ProgramAudio.Stereo;
 
             bool repeat = programInfo.@new == null;
-            string newID = programInfo.programID + "T" + startAt.Ticks + "C" + channel;
+            string newID = programInfo.programID + "T" + startAt.Ticks + "C" + channelId;
 
             if (programInfo.audioProperties != null)
             {
@@ -422,7 +264,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             
             var info = new ProgramInfo
             {
-                ChannelId = channel,
+                ChannelId = channelId,
                 Id = newID,
                 StartDate = startAt,
                 EndDate = endAt,
@@ -479,7 +321,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
                 else if (details.descriptions.description100 != null)
                 {
-                    info.ShortOverview = details.descriptions.description100[0].description;
+                    info.Overview = details.descriptions.description100[0].description;
                 }
             }
 
@@ -969,8 +811,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 throw new Exception("ListingsId required");
             }
 
-            await AddMetadata(info, new List<ChannelInfo>(), cancellationToken).ConfigureAwait(false);
-
             var token = await GetToken(info, cancellationToken);
 
             if (string.IsNullOrWhiteSpace(token))
@@ -997,39 +837,81 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Channel>(response);
                 _logger.Info("Found " + root.map.Count + " channels on the lineup on ScheduleDirect");
                 _logger.Info("Mapping Stations to Channel");
+
+                var allStations = root.stations ?? new List<ScheduleDirect.Station>();
+
                 foreach (ScheduleDirect.Map map in root.map)
                 {
-                    var channelNumber = map.logicalChannelNumber;
-
-                    if (string.IsNullOrWhiteSpace(channelNumber))
+                    var channelNumber = GetChannelNumber(map);
+                    
+                    var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
+                    if (station == null)
                     {
-                        channelNumber = map.channel;
-                    }
-                    if (string.IsNullOrWhiteSpace(channelNumber))
-                    {
-                        channelNumber = map.atscMajor + "." + map.atscMinor;
+                        station = new ScheduleDirect.Station
+                        {
+                            stationID = map.stationID
+                        };
                     }
-                    channelNumber = channelNumber.TrimStart('0');
 
                     var name = channelNumber;
-                    var station = GetStation(listingsId, channelNumber, null);
 
-                    if (station != null && !string.IsNullOrWhiteSpace(station.name))
-                    {
-                        name = station.name;
-                    }
-
-                    list.Add(new ChannelInfo
+                    var channelInfo = new ChannelInfo
                     {
                         Number = channelNumber,
                         Name = name
-                    });
+                    };
+
+                    if (station != null)
+                    {
+                        if (!string.IsNullOrWhiteSpace(station.name))
+                        {
+                            channelInfo.Name = station.name;
+                        }
+                       
+                        channelInfo.Id = station.stationID;
+                        channelInfo.CallSign = station.callsign;
+
+                        if (station.logo != null)
+                        {
+                            channelInfo.ImageUrl = station.logo.URL;
+                            channelInfo.HasImage = true;
+                        }
+                    }
+
+                    list.Add(channelInfo);
                 }
             }
 
             return list;
         }
 
+        private ScheduleDirect.Station GetStation(List<ScheduleDirect.Station> allStations, string channelNumber, string channelName)
+        {
+            if (!string.IsNullOrWhiteSpace(channelName))
+            {
+                channelName = NormalizeName(channelName);
+
+                var result = allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.callsign ?? string.Empty), channelName, StringComparison.OrdinalIgnoreCase));
+
+                if (result != null)
+                {
+                    return result;
+                }
+            }
+
+            if (!string.IsNullOrWhiteSpace(channelNumber))
+            {
+                return allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.stationID ?? string.Empty), channelNumber, StringComparison.OrdinalIgnoreCase));
+            }
+
+            return null;
+        }
+
+        private string NormalizeName(string value)
+        {
+            return value.Replace(" ", string.Empty).Replace("-", string.Empty);
+        }
+
         public class ScheduleDirect
         {
             public class Token

+ 28 - 27
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -106,8 +106,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return cacheFile;
         }
 
-        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
         {
+            if (string.IsNullOrWhiteSpace(channelId))
+            {
+                throw new ArgumentNullException("channelId");
+            }
+
             if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false))
             {
                 var length = endDateUtc - startDateUtc;
@@ -120,7 +125,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
             var reader = new XmlTvReader(path, GetLanguage());
 
-            var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken);
+            var results = reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken);
             return results.Select(p => GetProgramInfo(p, info));
         }
 
@@ -139,7 +144,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 StartDate = GetDate(p.StartDate),
                 Name = p.Title,
                 Overview = p.Description,
-                ShortOverview = p.Description,
                 ProductionYear = !p.CopyrightDate.HasValue ? (int?)null : p.CopyrightDate.Value.Year,
                 SeasonNumber = p.Episode == null ? null : p.Episode.Series,
                 IsSeries = p.Episode != null,
@@ -153,10 +157,29 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source),
                 OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null,
                 CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null,
-                SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null,
-                ShowId = ((p.Title ?? string.Empty) + (episodeTitle ?? string.Empty)).GetMD5().ToString("N")
+                SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null
             };
 
+            if (!string.IsNullOrWhiteSpace(p.ProgramId))
+            {
+                programInfo.ShowId = p.ProgramId;
+            }
+            else
+            {
+                var uniqueString = (p.Title ?? string.Empty) + (episodeTitle ?? string.Empty) + (p.IceTvEpisodeNumber ?? string.Empty);
+
+                if (programInfo.SeasonNumber.HasValue)
+                {
+                    uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture);
+                }
+                if (programInfo.EpisodeNumber.HasValue)
+                {
+                    uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture);
+                }
+
+                programInfo.ShowId = uniqueString.GetMD5().ToString("N");
+            }
+
             if (programInfo.IsMovie)
             {
                 programInfo.IsSeries = false;
@@ -176,28 +199,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return date;
         }
 
-        public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken)
-        {
-            // Add the channel image url
-            var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
-            var reader = new XmlTvReader(path, GetLanguage());
-            var results = reader.GetChannels().ToList();
-
-            if (channels != null)
-            {
-                foreach (var c in channels)
-                {
-                    var channelNumber = info.GetMappedChannel(c.Number);
-                    var match = results.FirstOrDefault(r => string.Equals(r.Id, channelNumber, StringComparison.OrdinalIgnoreCase));
-
-                    if (match != null && match.Icon != null && !String.IsNullOrEmpty(match.Icon.Source))
-                    {
-                        c.ImageUrl = match.Icon.Source;
-                    }
-                }
-            }
-        }
-
         public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
         {
             // Assume all urls are valid. check files for existence

+ 18 - 16
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -2112,7 +2112,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             if (timer == null)
             {
-                throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id));
+                throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id));
             }
 
             var service = GetService(timer.ServiceName);
@@ -2884,20 +2884,20 @@ namespace Emby.Server.Implementations.LiveTv
             _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
         }
 
-        public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
+        public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
         {
             var config = GetConfiguration();
 
             var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
-            listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
+            listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray();
 
-            if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
+            if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase))
             {
                 var list = listingsProviderInfo.ChannelMappings.ToList();
                 list.Add(new NameValuePair
                 {
-                    Name = tunerChannelNumber,
-                    Value = providerChannelNumber
+                    Name = tunerChannelId,
+                    Value = providerChannelId
                 });
                 listingsProviderInfo.ChannelMappings = list.ToArray();
             }
@@ -2917,31 +2917,33 @@ namespace Emby.Server.Implementations.LiveTv
 
             _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
 
-            return tunerChannelMappings.First(i => string.Equals(i.Number, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
+            return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
         }
 
-        public TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, List<NameValuePair> mappings, List<ChannelInfo> providerChannels)
+        public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, List<NameValuePair> mappings, List<ChannelInfo> epgChannels)
         {
             var result = new TunerChannelMapping
             {
-                Name = channel.Number + " " + channel.Name,
-                Number = channel.Number
+                Name = tunerChannel.Name,
+                Id = tunerChannel.TunerChannelId
             };
 
-            var mapping = mappings.FirstOrDefault(i => string.Equals(i.Name, channel.Number, StringComparison.OrdinalIgnoreCase));
-            var providerChannelNumber = channel.Number;
+            if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
+            {
+                result.Name = tunerChannel.Number + " " + result.Name;
+            }
 
-            if (mapping != null)
+            if (string.IsNullOrWhiteSpace(result.Id))
             {
-                providerChannelNumber = mapping.Value;
+                result.Id = tunerChannel.Id;
             }
 
-            var providerChannel = providerChannels.FirstOrDefault(i => string.Equals(i.Number, providerChannelNumber, StringComparison.OrdinalIgnoreCase));
+            var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
 
             if (providerChannel != null)
             {
-                result.ProviderChannelNumber = providerChannel.Number;
                 result.ProviderChannelName = providerChannel.Name;
+                result.ProviderChannelId = providerChannel.Id;
             }
 
             return result;

+ 41 - 13
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -76,6 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var channels = new List<M3UChannel>();
             string line;
             string extInf = "";
+
             while ((line = reader.ReadLine()) != null)
             {
                 line = line.Trim();
@@ -111,6 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                     extInf = "";
                 }
             }
+
             return channels;
         }
 
@@ -134,9 +136,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             channel.Name = GetChannelName(extInf, attributes);
             channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
 
-            if (attributes.TryGetValue("tvg-id", out value))
+            var channelId = GetTunerChannelId(attributes);
+            if (!string.IsNullOrWhiteSpace(channelId))
             {
-                channel.Id = value;
+                channel.Id = channelId;
+                channel.TunerChannelId = channelId;
             }
 
             return channel;
@@ -172,9 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 numberString = numberString.Trim();
             }
 
-            if (string.IsNullOrWhiteSpace(numberString) ||
-                string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
+            if (!IsValidChannelNumber(numberString))
             {
                 string value;
                 if (attributes.TryGetValue("tvg-id", out value))
@@ -192,9 +194,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 numberString = numberString.Trim();
             }
 
-            if (string.IsNullOrWhiteSpace(numberString) ||
-                string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
+            if (!IsValidChannelNumber(numberString))
             {
                 string value;
                 if (attributes.TryGetValue("channel-id", out value))
@@ -208,9 +208,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 numberString = numberString.Trim();
             }
 
-            if (string.IsNullOrWhiteSpace(numberString) ||
-                string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
+            if (!IsValidChannelNumber(numberString))
             {
                 numberString = null;
             }
@@ -225,8 +223,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 {
                     numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
 
-                    double value;
-                    if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
+                    if (!IsValidChannelNumber(numberString))
                     {
                         numberString = null;
                     }
@@ -236,6 +233,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             return numberString;
         }
 
+        private bool IsValidChannelNumber(string numberString)
+        {
+            if (string.IsNullOrWhiteSpace(numberString) ||
+                string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            double value;
+            if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
         private string GetChannelName(string extInf, Dictionary<string, string> attributes)
         {
             var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
@@ -281,6 +296,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             return name;
         }
 
+        private string GetTunerChannelId(Dictionary<string, string> attributes)
+        {
+            string result;
+            attributes.TryGetValue("tvg-id", out result);
+
+            if (string.IsNullOrWhiteSpace(result))
+            {
+                attributes.TryGetValue("channel-id", out result);
+            }
+
+            return result;
+        }
+
         private Dictionary<string, string> ParseExtInf(string line, out string remaining)
         {
             var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

+ 24 - 13
Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
@@ -11,10 +12,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public class MulticastStream
     {
-        private readonly List<QueueStream> _outputStreams = new List<QueueStream>();
+        private readonly ConcurrentDictionary<Guid,QueueStream> _outputStreams = new ConcurrentDictionary<Guid, QueueStream>();
         private const int BufferSize = 81920;
         private CancellationToken _cancellationToken;
         private readonly ILogger _logger;
+        private readonly ConcurrentQueue<byte[]> _sharedBuffer = new ConcurrentQueue<byte[]>();
 
         public MulticastStream(ILogger logger)
         {
@@ -35,17 +37,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 {
                     byte[] copy = new byte[bytesRead];
                     Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
-                   
-                    List<QueueStream> streams = null;
 
-                    lock (_outputStreams)
+                    _sharedBuffer.Enqueue(copy);
+
+                    while (_sharedBuffer.Count > 3000)
                     {
-                        streams = _outputStreams.ToList();
+                        byte[] bytes;
+                        _sharedBuffer.TryDequeue(out bytes);
                     }
 
-                    foreach (var stream in streams)
+                    var allStreams = _outputStreams.ToList();
+                    foreach (var stream in allStreams)
                     {
-                        stream.Queue(copy);
+                        stream.Value.Queue(copy);
                     }
 
                     if (onStarted != null)
@@ -70,11 +74,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 OnFinished = OnFinished
             };
 
-            lock (_outputStreams)
+            var initial = _sharedBuffer.ToList();
+            var list = new List<byte>();
+
+            foreach (var bytes in initial)
             {
-                _outputStreams.Add(result);
+                list.AddRange(bytes);
             }
 
+            _logger.Info("QueueStream started with {0} initial bytes", list.Count);
+
+            result.Queue(list.ToArray());
+
+            _outputStreams.TryAdd(result.Id, result);
+
             result.Start(_cancellationToken);
 
             return result.TaskCompletion.Task;
@@ -82,10 +95,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public void RemoveOutputStream(QueueStream stream)
         {
-            lock (_outputStreams)
-            {
-                _outputStreams.Remove(stream);
-            }
+            QueueStream removed;
+            _outputStreams.TryRemove(stream.Id, out removed);
         }
 
         private void OnFinished(QueueStream queueStream)

+ 3 - 13
Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs

@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public Action<QueueStream> OnFinished { get; set; }
         private readonly ILogger _logger;
-        private bool _isActive;
+        public Guid Id = Guid.NewGuid();
 
         public QueueStream(Stream outputStream, ILogger logger)
         {
@@ -30,10 +30,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public void Queue(byte[] bytes)
         {
-            if (_isActive)
-            {
-                _queue.Enqueue(bytes);
-            }
+            _queue.Enqueue(bytes);
         }
 
         public void Start(CancellationToken cancellationToken)
@@ -59,10 +56,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             try
             {
-                while (!cancellationToken.IsCancellationRequested)
+                while (true)
                 {
-                    _isActive = true;
-
                     var bytes = Dequeue();
                     if (bytes != null)
                     {
@@ -73,9 +68,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                         await Task.Delay(50, cancellationToken).ConfigureAwait(false);
                     }
                 }
-
-                TaskCompletion.TrySetResult(true);
-                _logger.Debug("QueueStream complete");
             }
             catch (OperationCanceledException)
             {
@@ -89,8 +81,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
             finally
             {
-                _isActive = false;
-
                 if (OnFinished != null)
                 {
                     OnFinished(this);

+ 1 - 1
Emby.Server.Implementations/Migrations/GuideMigration.cs

@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Migrations
 
         public async Task Run()
         {
-            var name = "GuideRefresh2";
+            var name = "GuideRefresh3";
 
             if (!_config.Configuration.Migrations.Contains(name, StringComparer.OrdinalIgnoreCase))
             {

+ 7 - 0
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -144,11 +144,18 @@ namespace Emby.Server.Implementations.TV
             // If viewing all next up for all series, remove first episodes
             // But if that returns empty, keep those first episodes (avoid completely empty view)
             var alwaysEnableFirstEpisode = !string.IsNullOrWhiteSpace(request.SeriesId);
+            var anyFound = false;
 
             return allNextUp
                 .Where(i =>
                 {
                     if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
+                    {
+                        anyFound = true;
+                        return true;
+                    }
+
+                    if (!anyFound && i.Item1 == DateTime.MinValue)
                     {
                         return true;
                     }

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

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
+using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Net;
@@ -246,7 +247,7 @@ namespace Emby.Server.Implementations.Udp
 
             try
             {
-                await _udpClient.SendAsync(bytes, bytes.Length, remoteEndPoint).ConfigureAwait(false);
+                await _udpClient.SendAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false);
 
                 _logger.Info("Udp message sent to {0}", remoteEndPoint);
             }

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

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Emby.XmlTv" version="1.0.3" targetFramework="portable45-net45+win8" />
+  <package id="Emby.XmlTv" version="1.0.5" targetFramework="portable45-net45+win8" />
   <package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCLRaw.core" version="1.1.1" targetFramework="portable45-net45+win8" />

+ 7 - 22
MediaBrowser.Api/BasePeriodicWebSocketListener.cs

@@ -23,8 +23,8 @@ namespace MediaBrowser.Api
         /// <summary>
         /// The _active connections
         /// </summary>
-        protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim>> ActiveConnections =
-            new List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim>>();
+        protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>> ActiveConnections =
+            new List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>>();
 
         /// <summary>
         /// Gets the name.
@@ -132,11 +132,9 @@ namespace MediaBrowser.Api
                 InitialDelayMs = dueTimeMs
             };
 
-            var semaphore = new SemaphoreSlim(1, 1);
-
             lock (ActiveConnections)
             {
-                ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim>(message.Connection, cancellationTokenSource, timer, state, semaphore));
+                ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>(message.Connection, cancellationTokenSource, timer, state));
             }
 
             if (timer != null)
@@ -153,7 +151,7 @@ namespace MediaBrowser.Api
         {
             var connection = (IWebSocketConnection)state;
 
-            Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim> tuple;
+            Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> tuple;
 
             lock (ActiveConnections)
             {
@@ -176,7 +174,7 @@ namespace MediaBrowser.Api
 
         protected void SendData(bool force)
         {
-            List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim>> tuples;
+            List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>> tuples;
 
             lock (ActiveConnections)
             {
@@ -204,14 +202,12 @@ namespace MediaBrowser.Api
             }
         }
 
-        private async void SendData(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim> tuple)
+        private async void SendData(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> tuple)
         {
             var connection = tuple.Item1;
 
             try
             {
-                await tuple.Item5.WaitAsync(tuple.Item2.Token).ConfigureAwait(false);
-
                 var state = tuple.Item4;
 
                 var data = await GetDataToSend(state).ConfigureAwait(false);
@@ -227,8 +223,6 @@ namespace MediaBrowser.Api
 
                     state.DateLastSendUtc = DateTime.UtcNow;
                 }
-
-                tuple.Item5.Release();
             }
             catch (OperationCanceledException)
             {
@@ -265,7 +259,7 @@ namespace MediaBrowser.Api
         /// Disposes the connection.
         /// </summary>
         /// <param name="connection">The connection.</param>
-        private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType, SemaphoreSlim> connection)
+        private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> connection)
         {
             Logger.Debug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
 
@@ -293,15 +287,6 @@ namespace MediaBrowser.Api
 
             }
 
-            try
-            {
-                connection.Item5.Dispose();
-            }
-            catch (ObjectDisposedException)
-            {
-
-            }
-
             ActiveConnections.Remove(connection);
         }
 

+ 1 - 1
MediaBrowser.Api/Images/RemoteImageService.cs

@@ -210,7 +210,7 @@ namespace MediaBrowser.Api.Images
         /// <returns>Task.</returns>
         private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request)
         {
-            await _providerManager.SaveImage(item, request.ImageUrl, null, request.Type, null, CancellationToken.None).ConfigureAwait(false);
+            await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false);
 
             await item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
         }

+ 0 - 2
MediaBrowser.Api/ItemUpdateService.cs

@@ -276,8 +276,6 @@ namespace MediaBrowser.Api
                 item.Tagline = request.Taglines.FirstOrDefault();
             }
 
-            item.ShortOverview = request.ShortOverview;
-
             item.Keywords = request.Keywords;
 
             if (request.Studios != null)

+ 4 - 4
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -640,8 +640,8 @@ namespace MediaBrowser.Api.LiveTv
     {
         [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string ProviderId { get; set; }
-        public string TunerChannelNumber { get; set; }
-        public string ProviderChannelNumber { get; set; }
+        public string TunerChannelId { get; set; }
+        public string ProviderChannelId { get; set; }
     }
 
     public class ChannelMappingOptions
@@ -765,7 +765,7 @@ namespace MediaBrowser.Api.LiveTv
 
         public async Task<object> Post(SetChannelMapping request)
         {
-            return await _liveTvManager.SetChannelMapping(request.ProviderId, request.TunerChannelNumber, request.ProviderChannelNumber).ConfigureAwait(false);
+            return await _liveTvManager.SetChannelMapping(request.ProviderId, request.TunerChannelId, request.ProviderChannelId).ConfigureAwait(false);
         }
 
         public async Task<object> Get(GetChannelMappingOptions request)
@@ -791,7 +791,7 @@ namespace MediaBrowser.Api.LiveTv
                 ProviderChannels = providerChannels.Select(i => new NameIdPair
                 {
                     Name = i.Name,
-                    Id = i.Number
+                    Id = i.TunerChannelId
 
                 }).ToList(),
 

+ 4 - 0
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -172,6 +172,10 @@
       <Project>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</Project>
       <Name>MediaBrowser.Controller</Name>
     </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj">
+      <Project>{0bd82fa6-eb8a-4452-8af5-74f9c3849451}</Project>
+      <Name>MediaBrowser.MediaEncoding</Name>
+    </ProjectReference>
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
       <Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project>
       <Name>MediaBrowser.Model</Name>

文件差异内容过多而无法显示
+ 45 - 1010
MediaBrowser.Api/Playback/BaseStreamingService.cs


+ 8 - 6
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -237,13 +237,15 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
+            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+
             var itsOffsetMs = 0;
 
             var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
 
-            var threads = GetNumberOfThreads(state, false);
+            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, false);
 
-            var inputModifier = GetInputModifier(state, true);
+            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
             // If isEncoding is true we're actually starting ffmpeg
             var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
@@ -265,9 +267,9 @@ namespace MediaBrowser.Api.Playback.Hls
 
                 return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
                     inputModifier,
-                    GetInputArgument(state),
+                    EncodingHelper.GetInputArgument(state, encodingOptions),
                     threads,
-                    GetMapArgs(state),
+                    EncodingHelper.GetMapArgs(state),
                     GetVideoArguments(state),
                     GetAudioArguments(state),
                     state.SegmentLength.ToString(UsCulture),
@@ -284,9 +286,9 @@ namespace MediaBrowser.Api.Playback.Hls
             var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
                 itsOffset,
                 inputModifier,
-                GetInputArgument(state),
+                    EncodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
-                GetMapArgs(state),
+                EncodingHelper.GetMapArgs(state),
                 GetVideoArguments(state),
                 GetAudioArguments(state),
                 state.SegmentLength.ToString(UsCulture),

+ 15 - 12
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -758,7 +758,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected override string GetAudioArguments(StreamState state)
         {
-            var codec = GetAudioEncoder(state);
+            var codec = EncodingHelper.GetAudioEncoder(state);
 
             if (!state.IsOutputVideo)
             {
@@ -811,7 +811,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
             }
 
-            args += " " + GetAudioFilterParam(state, true);
+            args += " " + EncodingHelper.GetAudioFilterParam(state, ApiEntryPoint.Instance.GetEncodingOptions(), true);
 
             return args;
         }
@@ -823,7 +823,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 return string.Empty;
             }
 
-            var codec = GetVideoEncoder(state);
+            var codec = EncodingHelper.GetVideoEncoder(state, ApiEntryPoint.Instance.GetEncodingOptions());
 
             var args = "-codec:v:0 " + codec;
 
@@ -835,7 +835,7 @@ namespace MediaBrowser.Api.Playback.Hls
             // See if we can save come cpu cycles by avoiding encoding
             if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
             {
-                if (state.VideoStream != null && IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
                 {
                     args += " -bsf:v h264_mp4toannexb";
                 }
@@ -849,20 +849,22 @@ namespace MediaBrowser.Api.Playback.Hls
 
                 var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
 
-                args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg;
+                var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+
+                args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
 
                 //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
 
                 // Add resolution params, if specified
                 if (!hasGraphicalSubs)
                 {
-                    args += GetOutputSizeParam(state, codec, EnableCopyTs(state));
+                    args += EncodingHelper.GetOutputSizeParam(state, codec, EnableCopyTs(state));
                 }
 
                 // This is for internal graphical subs
                 if (hasGraphicalSubs)
                 {
-                    args += GetGraphicalSubtitleParam(state, codec);
+                    args += EncodingHelper.GetGraphicalSubtitleParam(state, codec);
                 }
 
                 //args += " -flags -global_header";
@@ -884,15 +886,16 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
-            var threads = GetNumberOfThreads(state, false);
+            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, false);
 
-            var inputModifier = GetInputModifier(state, false);
+            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
             // If isEncoding is true we're actually starting ffmpeg
             var startNumber = GetStartNumber(state);
             var startNumberParam = isEncoding ? startNumber.ToString(UsCulture) : "0";
 
-            var mapArgs = state.IsOutputVideo ? GetMapArgs(state) : string.Empty;
+            var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
             var useGenericSegmenter = true;
 
             if (useGenericSegmenter)
@@ -909,7 +912,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
                 return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
                     inputModifier,
-                    GetInputArgument(state),
+                    EncodingHelper.GetInputArgument(state, encodingOptions),
                     threads,
                     mapArgs,
                     GetVideoArguments(state),
@@ -924,7 +927,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {6} -individual_header_trailer 0 -start_number {7} -hls_list_size {8} -y \"{9}\"",
                             inputModifier,
-                            GetInputArgument(state),
+                            EncodingHelper.GetInputArgument(state, encodingOptions),
                             threads,
                             mapArgs,
                             GetVideoArguments(state),

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

@@ -37,7 +37,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.String.</returns>
         protected override string GetAudioArguments(StreamState state)
         {
-            var codec = GetAudioEncoder(state);
+            var codec = EncodingHelper.GetAudioEncoder(state);
 
             if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
             {
@@ -60,7 +60,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
             }
 
-            args += " " + GetAudioFilterParam(state, true);
+            args += " " + EncodingHelper.GetAudioFilterParam(state, ApiEntryPoint.Instance.GetEncodingOptions(), true);
 
             return args;
         }
@@ -72,7 +72,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.String.</returns>
         protected override string GetVideoArguments(StreamState state)
         {
-            var codec = GetVideoEncoder(state);
+            var codec = EncodingHelper.GetVideoEncoder(state, ApiEntryPoint.Instance.GetEncodingOptions());
 
             var args = "-codec:v:0 " + codec;
 
@@ -85,7 +85,7 @@ namespace MediaBrowser.Api.Playback.Hls
             if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
             {
                 // if h264_mp4toannexb is ever added, do not use it for live tv
-                if (state.VideoStream != null && IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
                 {
                     args += " -bsf:v h264_mp4toannexb";
                 }
@@ -98,18 +98,19 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
 
-            args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg;
+            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+            args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
 
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
             {
-                args += GetOutputSizeParam(state, codec);
+                args += EncodingHelper.GetOutputSizeParam(state, codec);
             }
 
             // This is for internal graphical subs
             if (hasGraphicalSubs)
             {
-                args += GetGraphicalSubtitleParam(state, codec);
+                args += EncodingHelper.GetGraphicalSubtitleParam(state, codec);
             }
 
             args += " -flags -global_header";

+ 5 - 3
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -57,6 +57,8 @@ namespace MediaBrowser.Api.Playback.Progressive
 
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
+            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+
             var audioTranscodeParams = new List<string>();
 
             var bitrate = state.OutputAudioBitrate;
@@ -82,13 +84,13 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             const string vn = " -vn";
 
-            var threads = GetNumberOfThreads(state, false);
+            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, false);
 
-            var inputModifier = GetInputModifier(state);
+            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
             return string.Format("{0} {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"",
                 inputModifier,
-                GetInputArgument(state),
+                EncodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
                 vn,
                 string.Join(" ", audioTranscodeParams.ToArray()),

+ 39 - 12
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.IO;
+using System.Linq;
 using System.Threading.Tasks;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.IO;
@@ -95,8 +96,10 @@ namespace MediaBrowser.Api.Playback.Progressive
 
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
+            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+
             // Get the output codec name
-            var videoCodec = GetVideoEncoder(state);
+            var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
 
             var format = string.Empty;
             var keyFrame = string.Empty;
@@ -107,23 +110,46 @@ namespace MediaBrowser.Api.Playback.Progressive
                 format = " -f mp4 -movflags frag_keyframe+empty_moov";
             }
 
-            var threads = GetNumberOfThreads(state, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
+            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
+
+            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
-            var inputModifier = GetInputModifier(state);
+            var subtitleArguments = state.SubtitleStream != null &&
+                                    state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
+                ? GetSubtitleArguments(state)
+                : string.Empty;
 
-            return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7} -y \"{8}\"",
+            return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
                 inputModifier,
-                GetInputArgument(state),
+                EncodingHelper.GetInputArgument(state, encodingOptions),
                 keyFrame,
-                GetMapArgs(state),
+                EncodingHelper.GetMapArgs(state),
                 GetVideoArguments(state, videoCodec),
                 threads,
                 GetAudioArguments(state),
+                subtitleArguments,
                 format,
                 outputPath
                 ).Trim();
         }
 
+        private string GetSubtitleArguments(StreamState state)
+        {
+            var format = state.SupportedSubtitleCodecs.FirstOrDefault();
+            string codec;
+
+            if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase))
+            {
+                codec = "copy";
+            }
+            else
+            {
+                codec = format;
+            }
+
+            return " -codec:s:0 " + codec;
+        }
+
         /// <summary>
         /// Gets video arguments to pass to ffmpeg
         /// </summary>
@@ -141,7 +167,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
             {
-                if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
                 {
                     args += " -bsf:v h264_mp4toannexb";
                 }
@@ -170,7 +196,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
             {
-                var outputSizeParam = GetOutputSizeParam(state, videoCodec);
+                var outputSizeParam = EncodingHelper.GetOutputSizeParam(state, videoCodec);
                 args += outputSizeParam;
                 hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1;
             }
@@ -184,7 +210,8 @@ namespace MediaBrowser.Api.Playback.Progressive
                 args += " -avoid_negative_ts disabled -start_at_zero";
             }
 
-            var qualityParam = GetVideoQualityParam(state, videoCodec);
+            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+            var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, encodingOptions, GetDefaultH264Preset());
 
             if (!string.IsNullOrEmpty(qualityParam))
             {
@@ -194,7 +221,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // This is for internal graphical subs
             if (hasGraphicalSubs)
             {
-                args += GetGraphicalSubtitleParam(state, videoCodec);
+                args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec);
             }
 
             if (!state.RunTimeTicks.HasValue)
@@ -219,7 +246,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             }
 
             // Get the output codec name
-            var codec = GetAudioEncoder(state);
+            var codec = EncodingHelper.GetAudioEncoder(state);
 
             var args = "-codec:a:0 " + codec;
 
@@ -243,7 +270,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
             }
 
-            args += " " + GetAudioFilterParam(state, false);
+            args += " " + EncodingHelper.GetAudioFilterParam(state, ApiEntryPoint.Instance.GetEncodingOptions(), false);
 
             return args;
         }

+ 4 - 149
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Dlna;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Services;
 
 namespace MediaBrowser.Api.Playback
@@ -6,7 +7,7 @@ namespace MediaBrowser.Api.Playback
     /// <summary>
     /// Class StreamRequest
     /// </summary>
-    public class StreamRequest
+    public class StreamRequest : BaseEncodingJobOptions
     {
         /// <summary>
         /// Gets or sets the id.
@@ -28,45 +29,7 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string AudioCodec { get; set; }
 
-        /// <summary>
-        /// Gets or sets the start time ticks.
-        /// </summary>
-        /// <value>The start time ticks.</value>
-        [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public long? StartTimeTicks { get; set; }
-        
-        /// <summary>
-        /// Gets or sets the audio bit rate.
-        /// </summary>
-        /// <value>The audio bit rate.</value>
-        [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? AudioBitRate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the audio channels.
-        /// </summary>
-        /// <value>The audio channels.</value>
-        [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? AudioChannels { get; set; }
-
-        [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxAudioChannels { get; set; }
-
-        public int? TranscodingMaxAudioChannels { get; set; }
-
-        /// <summary>
-        /// Gets or sets the audio sample rate.
-        /// </summary>
-        /// <value>The audio sample rate.</value>
-        [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? AudioSampleRate { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this <see cref="StreamRequest" /> is static.
-        /// </summary>
-        /// <value><c>true</c> if static; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool Static { get; set; }
+        public string SubtitleCodec { get; set; }
 
         [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string DeviceProfileId { get; set; }
@@ -79,102 +42,6 @@ namespace MediaBrowser.Api.Playback
 
     public class VideoStreamRequest : StreamRequest
     {
-        /// <summary>
-        /// Gets or sets the video codec.
-        /// </summary>
-        /// <value>The video codec.</value>
-        [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string VideoCodec { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video bit rate.
-        /// </summary>
-        /// <value>The video bit rate.</value>
-        [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? VideoBitRate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the index of the audio stream.
-        /// </summary>
-        /// <value>The index of the audio stream.</value>
-        [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? AudioStreamIndex { get; set; }
-
-        /// <summary>
-        /// Gets or sets the index of the video stream.
-        /// </summary>
-        /// <value>The index of the video stream.</value>
-        [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? VideoStreamIndex { get; set; }
-
-        /// <summary>
-        /// Gets or sets the index of the subtitle stream.
-        /// </summary>
-        /// <value>The index of the subtitle stream.</value>
-        [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? SubtitleStreamIndex { get; set; }
-
-        /// <summary>
-        /// Gets or sets the width.
-        /// </summary>
-        /// <value>The width.</value>
-        [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Width { get; set; }
-
-        /// <summary>
-        /// Gets or sets the height.
-        /// </summary>
-        /// <value>The height.</value>
-        [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Height { get; set; }
-
-        /// <summary>
-        /// Gets or sets the width of the max.
-        /// </summary>
-        /// <value>The width of the max.</value>
-        [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxWidth { get; set; }
-
-        /// <summary>
-        /// Gets or sets the height of the max.
-        /// </summary>
-        /// <value>The height of the max.</value>
-        [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxHeight { get; set; }
-
-        [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxRefFrames { get; set; }
-
-        [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxVideoBitDepth { get; set; }
-        
-        /// <summary>
-        /// Gets or sets the framerate.
-        /// </summary>
-        /// <value>The framerate.</value>
-        [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
-        public float? Framerate { get; set; }
-
-        [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
-        public float? MaxFramerate { get; set; }
-        
-        /// <summary>
-        /// Gets or sets the profile.
-        /// </summary>
-        /// <value>The profile.</value>
-        [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Profile { get; set; }
-
-        /// <summary>
-        /// Gets or sets the level.
-        /// </summary>
-        /// <value>The level.</value>
-        [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; }
-
-        [ApiMember(Name = "SubtitleDeliveryMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public SubtitleDeliveryMethod SubtitleMethod { get; set; }
-        
         /// <summary>
         /// Gets a value indicating whether this instance has fixed resolution.
         /// </summary>
@@ -187,18 +54,6 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool EnableAutoStreamCopy { get; set; }
-
-        [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool CopyTimestamps { get; set; }
-
         public bool EnableSubtitlesInManifest { get; set; }
-        public bool RequireAvc { get; set; }
-
-        public VideoStreamRequest()
-        {
-            EnableAutoStreamCopy = true;
-        }
     }
 }

+ 17 - 91
MediaBrowser.Api/Playback/StreamState.cs

@@ -13,17 +13,28 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
+using MediaBrowser.MediaEncoding.Encoder;
 
 namespace MediaBrowser.Api.Playback
 {
-    public class StreamState : IDisposable
+    public class StreamState : EncodingJobInfo, IDisposable
     {
         private readonly ILogger _logger;
         private readonly IMediaSourceManager _mediaSourceManager;
 
         public string RequestedUrl { get; set; }
 
-        public StreamRequest Request { get; set; }
+        public StreamRequest Request
+        {
+            get { return (StreamRequest)BaseRequest; }
+            set
+            {
+                BaseRequest = value;
+
+                IsVideoRequest = VideoRequest != null;
+            }
+        }
+
         public TranscodingThrottler TranscodingThrottler { get; set; }
 
         public VideoStreamRequest VideoRequest
@@ -31,8 +42,6 @@ namespace MediaBrowser.Api.Playback
             get { return Request as VideoStreamRequest; }
         }
 
-        public Dictionary<string, string> RemoteHttpHeaders { get; set; }
-
         /// <summary>
         /// Gets or sets the log file stream.
         /// </summary>
@@ -40,35 +49,12 @@ namespace MediaBrowser.Api.Playback
         public Stream LogFileStream { get; set; }
         public IDirectStreamProvider DirectStreamProvider { get; set; }
 
-        public string InputContainer { get; set; }
-
-        public MediaSourceInfo MediaSource { get; set; }
-
-        public MediaStream AudioStream { get; set; }
-        public MediaStream VideoStream { get; set; }
-        public MediaStream SubtitleStream { get; set; }
-
-        /// <summary>
-        /// Gets or sets the iso mount.
-        /// </summary>
-        /// <value>The iso mount.</value>
-        public IIsoMount IsoMount { get; set; }
-
-        public string MediaPath { get; set; }
         public string WaitForPath { get; set; }
 
-        public MediaProtocol InputProtocol { get; set; }
-
         public bool IsOutputVideo
         {
             get { return Request is VideoStreamRequest; }
         }
-        public bool IsInputVideo { get; set; }
-
-        public VideoType VideoType { get; set; }
-        public IsoType? IsoType { get; set; }
-
-        public List<string> PlayableStreamFileNames { get; set; }
 
         public int SegmentLength
         {
@@ -116,39 +102,19 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        public long? RunTimeTicks;
-
-        public long? InputBitrate { get; set; }
-        public long? InputFileSize { get; set; }
-
-        public string OutputAudioSync = "1";
-        public string OutputVideoSync = "-1";
-
-        public List<string> SupportedAudioCodecs { get; set; }
-        public List<string> SupportedVideoCodecs { get; set; }
+        public List<string> SupportedSubtitleCodecs { get; set; }
         public string UserAgent { get; set; }
         public TranscodingJobType TranscodingType { get; set; }
 
-        public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType)
+        public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType) 
+            : base(logger)
         {
             _mediaSourceManager = mediaSourceManager;
             _logger = logger;
-            SupportedAudioCodecs = new List<string>();
-            SupportedVideoCodecs = new List<string>();
-            PlayableStreamFileNames = new List<string>();
-            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            SupportedSubtitleCodecs = new List<string>();
             TranscodingType = transcodingType;
         }
 
-        public string InputAudioSync { get; set; }
-        public string InputVideoSync { get; set; }
-
-        public bool DeInterlace { get; set; }
-
-        public bool ReadInputAtNativeFramerate { get; set; }
-
-        public TransportStreamTimestamp InputTimestamp { get; set; }
-
         public string MimeType { get; set; }
 
         public bool EstimateContentLength { get; set; }
@@ -209,23 +175,6 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        private void DisposeIsoMount()
-        {
-            if (IsoMount != null)
-            {
-                try
-                {
-                    IsoMount.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error disposing iso mount", ex);
-                }
-
-                IsoMount = null;
-            }
-        }
-
         private async void DisposeLiveStream()
         {
             if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
@@ -241,15 +190,8 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        public int InternalSubtitleStreamOffset { get; set; }
-
         public string OutputFilePath { get; set; }
-        public string OutputVideoCodec { get; set; }
-        public string OutputAudioCodec { get; set; }
-        public int? OutputAudioChannels;
-        public int? OutputAudioSampleRate;
         public int? OutputAudioBitrate;
-        public int? OutputVideoBitrate;
 
         public string ActualOutputVideoCodec
         {
@@ -295,8 +237,6 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        public string OutputContainer { get; set; }
-
         public DeviceProfile DeviceProfile { get; set; }
 
         public int? TotalOutputBitrate
@@ -444,20 +384,6 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream
-        /// </summary>
-        public double? TargetVideoLevel
-        {
-            get
-            {
-                var stream = VideoStream;
-                return !string.IsNullOrEmpty(VideoRequest.Level) && !Request.Static
-                    ? double.Parse(VideoRequest.Level, CultureInfo.InvariantCulture)
-                    : stream == null ? null : stream.Level;
-            }
-        }
-
         public TransportStreamTimestamp TargetTimestamp
         {
             get

+ 0 - 14
MediaBrowser.Api/SearchService.cs

@@ -189,24 +189,10 @@ namespace MediaBrowser.Api
                 result.Series = hasSeries.SeriesName;
             }
 
-            var season = item as Season;
-            if (season != null)
-            {
-                result.EpisodeCount = season.GetRecursiveChildren(i => i is Episode).Count;
-            }
-
-            var series = item as Series;
-            if (series != null)
-            {
-                result.EpisodeCount = series.GetRecursiveChildren(i => i is Episode).Count;
-            }
-
             var album = item as MusicAlbum;
 
             if (album != null)
             {
-                result.SongCount = album.Tracks.Count();
-
                 result.Artists = album.Artists.ToArray();
                 result.AlbumArtist = album.AlbumArtist;
             }

+ 1 - 2
MediaBrowser.Api/StartupWizardService.cs

@@ -111,15 +111,14 @@ namespace MediaBrowser.Api
 
         private void SetWizardFinishValues(ServerConfiguration config)
         {
-            config.EnableLocalizedGuids = true;
             config.EnableStandaloneMusicKeys = true;
             config.EnableCaseSensitiveItemIds = true;
             config.EnableFolderView = true;
-            config.EnableSimpleArtistDetection = true;
             config.SkipDeserializationForBasicTypes = true;
             config.SkipDeserializationForPrograms = true;
             config.SkipDeserializationForAudio = true;
             config.EnableSeriesPresentationUniqueKey = true;
+            config.EnableLocalizedGuids = true;
         }
 
         public void Post(UpdateStartupConfiguration request)

+ 5 - 1
MediaBrowser.Api/UserLibrary/PlaystateService.cs

@@ -213,6 +213,9 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
         public string MediaSourceId { get; set; }
 
+        [ApiMember(Name = "NextMediaType", Description = "The next media type that will play", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string NextMediaType { get; set; }
+
         /// <summary>
         /// Gets or sets the position ticks.
         /// </summary>
@@ -363,7 +366,8 @@ namespace MediaBrowser.Api.UserLibrary
                 PositionTicks = request.PositionTicks,
                 MediaSourceId = request.MediaSourceId,
                 PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId
+                LiveStreamId = request.LiveStreamId,
+                NextMediaType = request.NextMediaType
             });
         }
 

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

@@ -84,7 +84,6 @@ namespace MediaBrowser.Controller.Entities
 
         public long? Size { get; set; }
         public string Container { get; set; }
-        public string ShortOverview { get; set; }
         [IgnoreDataMember]
         public string Tagline { get; set; }
 
@@ -2263,11 +2262,6 @@ namespace MediaBrowser.Controller.Entities
                     ownedItem.Overview = item.Overview;
                     newOptions.ForceSave = true;
                 }
-                if (!string.Equals(item.ShortOverview, ownedItem.ShortOverview, StringComparison.Ordinal))
-                {
-                    ownedItem.ShortOverview = item.ShortOverview;
-                    newOptions.ForceSave = true;
-                }
                 if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
                 {
                     ownedItem.OfficialRating = item.OfficialRating;

+ 0 - 1
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -172,7 +172,6 @@ namespace MediaBrowser.Controller.Entities
                 case ItemFields.ProductionLocations:
                 case ItemFields.Keywords:
                 case ItemFields.Taglines:
-                case ItemFields.ShortOverview:
                 case ItemFields.CustomRating:
                 case ItemFields.DateCreated:
                 case ItemFields.SortName:

+ 2 - 4
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -788,7 +788,7 @@ namespace MediaBrowser.Controller.Entities
                 query.IsVirtualUnaired,
                 query.IsUnaired);
 
-            if (collapseBoxSetItems)
+            if (collapseBoxSetItems && user != null)
             {
                 items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user, configurationManager);
             }
@@ -1119,13 +1119,11 @@ namespace MediaBrowser.Controller.Entities
             InternalItemsQuery query,
             ILibraryManager libraryManager, bool enableSorting)
         {
-            var user = query.User;
-
             items = items.DistinctBy(i => i.GetPresentationUniqueKey(), StringComparer.OrdinalIgnoreCase);
 
             if (query.SortBy.Length > 0)
             {
-                items = libraryManager.Sort(items, user, query.SortBy, query.SortOrder);
+                items = libraryManager.Sort(items, query.User, query.SortBy, query.SortOrder);
             }
 
             var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();

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

@@ -456,7 +456,7 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         /// <param name="item">The item.</param>
         /// <returns>IEnumerable&lt;Folder&gt;.</returns>
-        IEnumerable<Folder> GetCollectionFolders(BaseItem item);
+        List<Folder> GetCollectionFolders(BaseItem item);
 
         LibraryOptions GetLibraryOptions(BaseItem item);
 

+ 4 - 0
MediaBrowser.Controller/LiveTv/ChannelInfo.cs

@@ -25,6 +25,10 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The id of the channel.</value>
         public string Id { get; set; }
 
+        public string TunerChannelId { get; set; }
+
+        public string CallSign { get; set; }
+
         /// <summary>
         /// Gets or sets the tuner host identifier.
         /// </summary>

+ 1 - 2
MediaBrowser.Controller/LiveTv/IListingsProvider.cs

@@ -11,8 +11,7 @@ namespace MediaBrowser.Controller.LiveTv
     {
         string Name { get; }
         string Type { get; }
-        Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
-        Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken);
+        Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
         Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings);
         Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location);
         Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken);

+ 0 - 1
MediaBrowser.Controller/LiveTv/TimerInfo.cs

@@ -114,7 +114,6 @@ namespace MediaBrowser.Controller.LiveTv
         public bool IsRepeat { get; set; }
         public string HomePageUrl { get; set; }
         public float? CommunityRating { get; set; }
-        public string ShortOverview { get; set; }
         public string OfficialRating { get; set; }
         public List<string> Genres { get; set; }
         public string RecordingPath { get; set; }

+ 2 - 2
MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs

@@ -3,8 +3,8 @@
     public class TunerChannelMapping
     {
         public string Name { get; set; }
-        public string Number { get; set; }
-        public string ProviderChannelNumber { get; set; }
         public string ProviderChannelName { get; set; }
+        public string ProviderChannelId { get; set; }
+        public string Id { get; set; }
     }
 }

+ 159 - 39
MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs

@@ -1,51 +1,22 @@
-using MediaBrowser.Model.Dlna;
+using System.Globalization;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Services;
 
 namespace MediaBrowser.Controller.MediaEncoding
 {
-    public class EncodingJobOptions
+    public class EncodingJobOptions : BaseEncodingJobOptions
     {
-        public string OutputContainer { get; set; }
         public string OutputDirectory { get; set; }
 
-        public long? StartTimeTicks { get; set; }
-        public int? Width { get; set; }
-        public int? Height { get; set; }
-        public int? MaxWidth { get; set; }
-        public int? MaxHeight { get; set; }
-        public bool Static = false;
-        public float? Framerate { get; set; }
-        public float? MaxFramerate { get; set; }
-        public string Profile { get; set; }
-        public int? Level { get; set; }
-
         public string DeviceId { get; set; }
         public string ItemId { get; set; }
         public string MediaSourceId { get; set; }
         public string AudioCodec { get; set; }
 
-        public bool EnableAutoStreamCopy { get; set; }
-
-        public int? MaxAudioChannels { get; set; }
-        public int? AudioChannels { get; set; }
-        public int? AudioBitRate { get; set; }
-        public int? AudioSampleRate { get; set; }
-   
         public DeviceProfile DeviceProfile { get; set; }
         public EncodingContext Context { get; set; }
 
-        public string VideoCodec { get; set; }
-
-        public int? TranscodingMaxAudioChannels { get; set; }
-        public int? VideoBitRate { get; set; }
-        public int? AudioStreamIndex { get; set; }
-        public int? VideoStreamIndex { get; set; }
-        public int? SubtitleStreamIndex { get; set; }
-        public int? MaxRefFrames { get; set; }
-        public int? MaxVideoBitDepth { get; set; }
-        public int? CpuCoreLimit { get; set; }
         public bool ReadInputAtNativeFramerate { get; set; }
-        public SubtitleDeliveryMethod SubtitleMethod { get; set; }
-        public bool CopyTimestamps { get; set; }
 
         /// <summary>
         /// Gets a value indicating whether this instance has fixed resolution.
@@ -59,11 +30,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
         }
 
-        public EncodingJobOptions()
-        {
-            
-        }
-
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile)
         {
             OutputContainer = info.Container;
@@ -72,7 +39,6 @@ namespace MediaBrowser.Controller.MediaEncoding
             MaxHeight = info.MaxHeight;
             MaxFramerate = info.MaxFramerate;
             Profile = info.VideoProfile;
-            Level = info.VideoLevel;
             ItemId = info.ItemId;
             MediaSourceId = info.MediaSourceId;
             AudioCodec = info.TargetAudioCodec;
@@ -93,6 +59,160 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 SubtitleStreamIndex = info.SubtitleStreamIndex;
             }
+
+            if (info.VideoLevel.HasValue)
+            {
+                Level = info.VideoLevel.Value.ToString(_usCulture);
+            }
+        }
+    }
+
+    // For now until api and media encoding layers are unified
+    public class BaseEncodingJobOptions
+    {
+        [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool EnableAutoStreamCopy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio sample rate.
+        /// </summary>
+        /// <value>The audio sample rate.</value>
+        [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? AudioSampleRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio bit rate.
+        /// </summary>
+        /// <value>The audio bit rate.</value>
+        [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? AudioBitRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio channels.
+        /// </summary>
+        /// <value>The audio channels.</value>
+        [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? AudioChannels { get; set; }
+
+        [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? MaxAudioChannels { get; set; }
+
+        [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool Static { get; set; }
+
+        /// <summary>
+        /// Gets or sets the profile.
+        /// </summary>
+        /// <value>The profile.</value>
+        [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Profile { get; set; }
+
+        /// <summary>
+        /// Gets or sets the level.
+        /// </summary>
+        /// <value>The level.</value>
+        [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; }
+
+        /// <summary>
+        /// Gets or sets the framerate.
+        /// </summary>
+        /// <value>The framerate.</value>
+        [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
+        public float? Framerate { get; set; }
+
+        [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
+        public float? MaxFramerate { get; set; }
+
+        [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool CopyTimestamps { get; set; }
+
+        /// <summary>
+        /// Gets or sets the start time ticks.
+        /// </summary>
+        /// <value>The start time ticks.</value>
+        [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public long? StartTimeTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the width.
+        /// </summary>
+        /// <value>The width.</value>
+        [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Width { get; set; }
+
+        /// <summary>
+        /// Gets or sets the height.
+        /// </summary>
+        /// <value>The height.</value>
+        [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Height { get; set; }
+
+        /// <summary>
+        /// Gets or sets the width of the max.
+        /// </summary>
+        /// <value>The width of the max.</value>
+        [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? MaxWidth { get; set; }
+
+        /// <summary>
+        /// Gets or sets the height of the max.
+        /// </summary>
+        /// <value>The height of the max.</value>
+        [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? MaxHeight { get; set; }
+
+        /// <summary>
+        /// Gets or sets the video bit rate.
+        /// </summary>
+        /// <value>The video bit rate.</value>
+        [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? VideoBitRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the subtitle stream.
+        /// </summary>
+        /// <value>The index of the subtitle stream.</value>
+        [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? SubtitleStreamIndex { get; set; }
+
+        [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+
+        [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? MaxRefFrames { get; set; }
+
+        [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? MaxVideoBitDepth { get; set; }
+        public bool RequireAvc { get; set; }
+        public int? TranscodingMaxAudioChannels { get; set; }
+        public int? CpuCoreLimit { get; set; }
+        public string OutputContainer { get; set; }
+
+        /// <summary>
+        /// Gets or sets the video codec.
+        /// </summary>
+        /// <value>The video codec.</value>
+        [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string VideoCodec { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the audio stream.
+        /// </summary>
+        /// <value>The index of the audio stream.</value>
+        [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? AudioStreamIndex { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the video stream.
+        /// </summary>
+        /// <value>The index of the video stream.</value>
+        [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? VideoStreamIndex { get; set; }
+
+        public BaseEncodingJobOptions()
+        {
+            EnableAutoStreamCopy = true;
         }
     }
 }

+ 1 - 2
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -47,12 +47,11 @@ namespace MediaBrowser.Controller.Providers
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="url">The URL.</param>
-        /// <param name="resourcePool">The resource pool.</param>
         /// <param name="type">The type.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task SaveImage(IHasImages item, string url, SemaphoreSlim resourcePool, ImageType type, int? imageIndex, CancellationToken cancellationToken);
+        Task SaveImage(IHasImages item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken);
 
         /// <summary>
         /// Saves the image.

+ 0 - 12
MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

@@ -294,18 +294,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
                         break;
                     }
 
-                case "ShortOverview":
-                    {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            item.ShortOverview = val;
-                        }
-
-                        break;
-                    }
-
                 case "CriticRatingSummary":
                     {
                         var val = reader.ReadElementContentAsString();

+ 0 - 5
MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs

@@ -85,7 +85,6 @@ namespace MediaBrowser.LocalMetadata.Savers
                     "MusicbrainzId",
 
                     "Overview",
-                    "ShortOverview",
                     "Persons",
                     "PlotKeywords",
                     "PremiereDate",
@@ -351,10 +350,6 @@ namespace MediaBrowser.LocalMetadata.Savers
             {
                 writer.WriteElementString("OriginalTitle", item.OriginalTitle);
             }
-            if (!string.IsNullOrEmpty(item.ShortOverview))
-            {
-                writer.WriteElementString("ShortOverview", item.ShortOverview);
-            }
             if (!string.IsNullOrEmpty(item.CustomRating))
             {
                 writer.WriteElementString("CustomRating", item.CustomRating);

+ 5 - 3
MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs

@@ -42,9 +42,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 }
             }
 
-            var threads = GetNumberOfThreads(state, false);
+            var encodingOptions = GetEncodingOptions();
 
-            var inputModifier = GetInputModifier(state);
+            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, false);
+
+            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
             var albumCoverInput = string.Empty;
             var mapArgs = string.Empty;
@@ -67,7 +69,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             var result = string.Format("{0} {1}{6}{7} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{8} -y \"{5}\"",
                 inputModifier,
-                GetInputArgument(state),
+                EncodingHelper.GetInputArgument(state, GetEncodingOptions()),
                 threads,
                 vn,
                 string.Join(" ", audioTranscodeParams.ToArray()),

+ 7 - 791
MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs

@@ -36,6 +36,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
+        protected EncodingHelper EncodingHelper;
+
         protected BaseEncoder(MediaEncoder mediaEncoder,
             ILogger logger,
             IServerConfigurationManager configurationManager,
@@ -56,6 +58,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
             SubtitleEncoder = subtitleEncoder;
             MediaSourceManager = mediaSourceManager;
             ProcessFactory = processFactory;
+
+            EncodingHelper = new EncodingHelper(MediaEncoder, ConfigurationManager, FileSystem, SubtitleEncoder);
         }
 
         public async Task<EncodingJob> Start(EncodingJobOptions options,
@@ -63,7 +67,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             CancellationToken cancellationToken)
         {
             var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager)
-                .CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
+                .CreateJob(options, EncodingHelper, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
 
             encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
             FileSystem.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
@@ -285,72 +289,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return null;
         }
 
-        /// <summary>
-        /// Gets the number of threads.
-        /// </summary>
-        /// <returns>System.Int32.</returns>
-        protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
-        {
-            return job.Options.CpuCoreLimit ?? 0;
-        }
-
-        protected string GetInputModifier(EncodingJob state, bool genPts = true)
-        {
-            var inputModifier = string.Empty;
-
-            var probeSize = GetProbeSizeArgument(state);
-            inputModifier += " " + probeSize;
-            inputModifier = inputModifier.Trim();
-
-            var userAgentParam = GetUserAgentParam(state);
-
-            if (!string.IsNullOrWhiteSpace(userAgentParam))
-            {
-                inputModifier += " " + userAgentParam;
-            }
-
-            inputModifier = inputModifier.Trim();
-
-            inputModifier += " " + GetFastSeekCommandLineParameter(state.Options);
-            inputModifier = inputModifier.Trim();
-
-            if (state.IsVideoRequest && genPts)
-            {
-                inputModifier += " -fflags +genpts";
-            }
-
-            if (!string.IsNullOrEmpty(state.InputAudioSync))
-            {
-                inputModifier += " -async " + state.InputAudioSync;
-            }
-
-            if (!string.IsNullOrEmpty(state.InputVideoSync))
-            {
-                inputModifier += " -vsync " + state.InputVideoSync;
-            }
-
-            if (state.ReadInputAtNativeFramerate)
-            {
-                inputModifier += " -re";
-            }
-
-            var videoDecoder = GetVideoDecoder(state);
-            if (!string.IsNullOrWhiteSpace(videoDecoder))
-            {
-                inputModifier += " " + videoDecoder;
-            }
-
-            //if (state.IsVideoRequest)
-            //{
-            //    if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase))
-            //    {
-            //        //inputModifier += " -noaccurate_seek";
-            //    }
-            //}
-
-            return inputModifier;
-        }
-
         /// <summary>
         /// Gets the name of the output video codec
         /// </summary>
@@ -405,130 +343,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return null;
         }
 
-        private string GetUserAgentParam(EncodingJob state)
-        {
-            string useragent = null;
-
-            state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
-
-            if (!string.IsNullOrWhiteSpace(useragent))
-            {
-                return "-user-agent \"" + useragent + "\"";
-            }
-
-            return string.Empty;
-        }
-
-        /// <summary>
-        /// Gets the probe size argument.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        private string GetProbeSizeArgument(EncodingJob state)
-        {
-            if (state.PlayableStreamFileNames.Count > 0)
-            {
-                return MediaEncoder.GetProbeSizeAndAnalyzeDurationArgument(state.PlayableStreamFileNames.ToArray(), state.InputProtocol);
-            }
-
-            return MediaEncoder.GetProbeSizeAndAnalyzeDurationArgument(new[] { state.MediaPath }, state.InputProtocol);
-        }
-
-        /// <summary>
-        /// Gets the fast seek command line parameter.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.String.</returns>
-        /// <value>The fast seek command line parameter.</value>
-        protected string GetFastSeekCommandLineParameter(EncodingJobOptions request)
-        {
-            var time = request.StartTimeTicks ?? 0;
-
-            if (time > 0)
-            {
-                return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time));
-            }
-
-            return string.Empty;
-        }
-
-        /// <summary>
-        /// Gets the input argument.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected string GetInputArgument(EncodingJob state)
-        {
-            var arg = string.Format("-i {0}", GetInputPathArgument(state));
-
-            if (state.SubtitleStream != null && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode)
-            {
-                if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
-                {
-                    if (state.VideoStream != null && state.VideoStream.Width.HasValue)
-                    {
-                        // This is hacky but not sure how to get the exact subtitle resolution
-                        double height = state.VideoStream.Width.Value;
-                        height /= 16;
-                        height *= 9;
-
-                        arg += string.Format(" -canvas_size {0}:{1}", state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture), Convert.ToInt32(height).ToString(CultureInfo.InvariantCulture));
-                    }
-
-                    var subtitlePath = state.SubtitleStream.Path;
-
-                    if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase))
-                    {
-                        var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
-                        if (FileSystem.FileExists(idxFile))
-                        {
-                            subtitlePath = idxFile;
-                        }
-                    }
-
-                    arg += " -i \"" + subtitlePath + "\"";
-                }
-            }
-
-            if (state.IsVideoRequest)
-            {
-                var encodingOptions = GetEncodingOptions();
-                var videoEncoder = EncodingJobFactory.GetVideoEncoder(MediaEncoder, state, encodingOptions);
-                if (videoEncoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode;
-                    var hwOutputFormat = "vaapi";
-
-                    if (hasGraphicalSubs)
-                    {
-                        hwOutputFormat = "yuv420p";
-                    }
-
-                    arg = "-hwaccel vaapi -hwaccel_output_format " + hwOutputFormat + " -vaapi_device " + encodingOptions.VaapiDevice + " " + arg;
-                }
-            }
-
-            return arg.Trim();
-        }
-
-        private string GetInputPathArgument(EncodingJob state)
-        {
-            var protocol = state.InputProtocol;
-            var mediaPath = state.MediaPath ?? string.Empty;
-
-            var inputPath = new[] { mediaPath };
-
-            if (state.IsInputVideo)
-            {
-                if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
-                {
-                    inputPath = MediaEncoderHelpers.GetInputArgument(FileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
-                }
-            }
-
-            return MediaEncoder.GetInputArgument(inputPath, protocol);
-        }
-
         private async Task AcquireResources(EncodingJob state, CancellationToken cancellationToken)
         {
             if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
@@ -544,11 +358,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
                 }, false, cancellationToken).ConfigureAwait(false);
 
-                AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.Options);
+                EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, null);
 
                 if (state.IsVideoRequest)
                 {
-                    EncodingJobFactory.TryStreamCopy(state, state.Options);
+                    EncodingHelper.TryStreamCopy(state);
                 }
             }
 
@@ -557,603 +371,5 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 await Task.Delay(state.MediaSource.BufferMs.Value, cancellationToken).ConfigureAwait(false);
             }
         }
-
-        private void AttachMediaSourceInfo(EncodingJob state,
-          MediaSourceInfo mediaSource,
-          EncodingJobOptions videoRequest)
-        {
-            EncodingJobFactory.AttachMediaSourceInfo(state, mediaSource, videoRequest);
-        }
-
-        /// <summary>
-        /// Gets the internal graphical subtitle param.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="outputVideoCodec">The output video codec.</param>
-        /// <returns>System.String.</returns>
-        protected async Task<string> GetGraphicalSubtitleParam(EncodingJob state, string outputVideoCodec)
-        {
-            var outputSizeParam = string.Empty;
-
-            var request = state.Options;
-
-            // Add resolution params, if specified
-            if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
-            {
-                outputSizeParam = await GetOutputSizeParam(state, outputVideoCodec).ConfigureAwait(false);
-                outputSizeParam = outputSizeParam.TrimEnd('"');
-
-                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
-                {
-                    outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase));
-                }
-                else
-                {
-                    outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
-                }
-            }
-
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && outputSizeParam.Length == 0)
-            {
-                outputSizeParam = ",format=nv12|vaapi,hwupload";
-            }
-
-            var videoSizeParam = string.Empty;
-
-            if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
-            {
-                videoSizeParam = string.Format(",scale={0}:{1}", state.VideoStream.Width.Value.ToString(UsCulture), state.VideoStream.Height.Value.ToString(UsCulture));
-            }
-
-            var mapPrefix = state.SubtitleStream.IsExternal ?
-                1 :
-                0;
-
-            var subtitleStreamIndex = state.SubtitleStream.IsExternal
-                ? 0
-                : state.SubtitleStream.Index;
-
-            return string.Format(" -filter_complex \"[{0}:{1}]format=yuva444p{4},lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:{2}] [sub] overlay{3}\"",
-                mapPrefix.ToString(UsCulture),
-                subtitleStreamIndex.ToString(UsCulture),
-                state.VideoStream.Index.ToString(UsCulture),
-                outputSizeParam,
-                videoSizeParam);
-        }
-
-        /// <summary>
-        /// Gets the video bitrate to specify on the command line
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="videoEncoder">The video codec.</param>
-        /// <returns>System.String.</returns>
-        protected string GetVideoQualityParam(EncodingJob state, string videoEncoder)
-        {
-            var param = string.Empty;
-
-            var isVc1 = state.VideoStream != null &&
-                string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
-
-            if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-preset superfast";
-
-                param += " -crf 23";
-            }
-
-            else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-preset fast";
-
-                param += " -crf 28";
-            }
-
-            // h264 (h264_qsv)
-            else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-preset 7 -look_ahead 0";
-
-            }
-
-            // h264 (h264_nvenc)
-            else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-preset llhq";
-            }
-
-            // webm
-            else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase))
-            {
-                // Values 0-3, 0 being highest quality but slower
-                var profileScore = 0;
-
-                string crf;
-                var qmin = "0";
-                var qmax = "50";
-
-                crf = "10";
-
-                if (isVc1)
-                {
-                    profileScore++;
-                }
-
-                // Max of 2
-                profileScore = Math.Min(profileScore, 2);
-
-                // http://www.webmproject.org/docs/encoder-parameters/
-                param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
-                    profileScore.ToString(UsCulture),
-                    crf,
-                    qmin,
-                    qmax);
-            }
-
-            else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
-            }
-
-            // asf/wmv
-            else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-qmin 2";
-            }
-
-            else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-mbd 2";
-            }
-
-            param += GetVideoBitrateParam(state, videoEncoder);
-
-            var framerate = GetFramerateParam(state);
-            if (framerate.HasValue)
-            {
-                param += string.Format(" -r {0}", framerate.Value.ToString(UsCulture));
-            }
-
-            if (!string.IsNullOrEmpty(state.OutputVideoSync))
-            {
-                param += " -vsync " + state.OutputVideoSync;
-            }
-
-            if (!string.IsNullOrEmpty(state.Options.Profile))
-            {
-                if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
-                    !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
-                {
-                    // not supported by h264_omx
-                    param += " -profile:v " + state.Options.Profile;
-                }
-            }
-
-            var levelString = state.Options.Level.HasValue ? state.Options.Level.Value.ToString(CultureInfo.InvariantCulture) : null;
-
-            if (!string.IsNullOrEmpty(levelString))
-            {
-                levelString = NormalizeTranscodingLevel(state.OutputVideoCodec, levelString);
-
-                // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
-                // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
-                if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
-                {
-                    switch (levelString)
-                    {
-                        case "30":
-                            param += " -level 3.0";
-                            break;
-                        case "31":
-                            param += " -level 3.1";
-                            break;
-                        case "32":
-                            param += " -level 3.2";
-                            break;
-                        case "40":
-                            param += " -level 4.0";
-                            break;
-                        case "41":
-                            param += " -level 4.1";
-                            break;
-                        case "42":
-                            param += " -level 4.2";
-                            break;
-                        case "50":
-                            param += " -level 5.0";
-                            break;
-                        case "51":
-                            param += " -level 5.1";
-                            break;
-                        case "52":
-                            param += " -level 5.2";
-                            break;
-                        default:
-                            param += " -level " + levelString;
-                            break;
-                    }
-                }
-                else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
-                {
-                    param += " -level " + levelString;
-                }
-            }
-
-            if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
-                !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) &&
-                !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
-            {
-                param = "-pix_fmt yuv420p " + param;
-            }
-
-            return param;
-        }
-
-        private string NormalizeTranscodingLevel(string videoCodec, string level)
-        {
-            double requestLevel;
-
-            // Clients may direct play higher than level 41, but there's no reason to transcode higher
-            if (double.TryParse(level, NumberStyles.Any, UsCulture, out requestLevel))
-            {
-                if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
-                {
-                    if (requestLevel > 41)
-                    {
-                        return "41";
-                    }
-                }
-            }
-
-            return level;
-        }
-
-        protected string GetVideoBitrateParam(EncodingJob state, string videoCodec)
-        {
-            var bitrate = state.OutputVideoBitrate;
-
-            if (bitrate.HasValue)
-            {
-                if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
-                {
-                    // With vpx when crf is used, b:v becomes a max rate
-                    // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
-                    return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
-                }
-
-                if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
-                {
-                    return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
-                }
-
-                if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
-                {
-                    // h264
-                    return string.Format(" -maxrate {0} -bufsize {1}",
-                        bitrate.Value.ToString(UsCulture),
-                        (bitrate.Value * 2).ToString(UsCulture));
-                }
-
-                // h264
-                return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}",
-                    bitrate.Value.ToString(UsCulture),
-                    (bitrate.Value * 2).ToString(UsCulture));
-            }
-
-            return string.Empty;
-        }
-
-        protected double? GetFramerateParam(EncodingJob state)
-        {
-            if (state.Options != null)
-            {
-                if (state.Options.Framerate.HasValue)
-                {
-                    return state.Options.Framerate.Value;
-                }
-
-                var maxrate = state.Options.MaxFramerate;
-
-                if (maxrate.HasValue && state.VideoStream != null)
-                {
-                    var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
-
-                    if (contentRate.HasValue && contentRate.Value > maxrate.Value)
-                    {
-                        return maxrate;
-                    }
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets the map args.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected virtual string GetMapArgs(EncodingJob state)
-        {
-            // If we don't have known media info
-            // If input is video, use -sn to drop subtitles
-            // Otherwise just return empty
-            if (state.VideoStream == null && state.AudioStream == null)
-            {
-                return state.IsInputVideo ? "-sn" : string.Empty;
-            }
-
-            // We have media info, but we don't know the stream indexes
-            if (state.VideoStream != null && state.VideoStream.Index == -1)
-            {
-                return "-sn";
-            }
-
-            // We have media info, but we don't know the stream indexes
-            if (state.AudioStream != null && state.AudioStream.Index == -1)
-            {
-                return state.IsInputVideo ? "-sn" : string.Empty;
-            }
-
-            var args = string.Empty;
-
-            if (state.VideoStream != null)
-            {
-                args += string.Format("-map 0:{0}", state.VideoStream.Index);
-            }
-            else
-            {
-                // No known video stream
-                args += "-vn";
-            }
-
-            if (state.AudioStream != null)
-            {
-                args += string.Format(" -map 0:{0}", state.AudioStream.Index);
-            }
-
-            else
-            {
-                args += " -map -0:a";
-            }
-
-            if (state.SubtitleStream == null || state.Options.SubtitleMethod == SubtitleDeliveryMethod.Hls)
-            {
-                args += " -map -0:s";
-            }
-            else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
-            {
-                args += " -map 1:0 -sn";
-            }
-
-            return args;
-        }
-
-        /// <summary>
-        /// Determines whether the specified stream is H264.
-        /// </summary>
-        /// <param name="stream">The stream.</param>
-        /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
-        protected bool IsH264(MediaStream stream)
-        {
-            var codec = stream.Codec ?? string.Empty;
-
-            return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
-        }
-
-        /// <summary>
-        /// If we're going to put a fixed size on the command line, this will calculate it
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="outputVideoCodec">The output video codec.</param>
-        /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
-        /// <returns>System.String.</returns>
-        protected async Task<string> GetOutputSizeParam(EncodingJob state,
-            string outputVideoCodec,
-            bool allowTimeStampCopy = true)
-        {
-            // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
-
-            var request = state.Options;
-
-            var filters = new List<string>();
-
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
-            {
-                filters.Add("format=nv12|vaapi");
-                filters.Add("hwupload");
-            }
-            else if (state.DeInterlace && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
-            {
-                filters.Add("yadif=0:-1:0");
-            }
-
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
-            {
-                // Work around vaapi's reduced scaling features
-                var scaler = "scale_vaapi";
-
-                // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions
-                // (outputWidth, outputHeight). The user may request precise output dimensions or maximum
-                // output dimensions. Output dimensions are guaranteed to be even.
-                decimal inputWidth = Convert.ToDecimal(state.VideoStream.Width);
-                decimal inputHeight = Convert.ToDecimal(state.VideoStream.Height);
-                decimal outputWidth = request.Width.HasValue ? Convert.ToDecimal(request.Width.Value) : inputWidth;
-                decimal outputHeight = request.Height.HasValue ? Convert.ToDecimal(request.Height.Value) : inputHeight;
-                decimal maximumWidth = request.MaxWidth.HasValue ? Convert.ToDecimal(request.MaxWidth.Value) : outputWidth;
-                decimal maximumHeight = request.MaxHeight.HasValue ? Convert.ToDecimal(request.MaxHeight.Value) : outputHeight;
-
-                if (outputWidth > maximumWidth || outputHeight > maximumHeight)
-                {
-                    var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight);
-                    outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale));
-                    outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale));
-                }
-
-                outputWidth = 2 * Math.Truncate(outputWidth / 2);
-                outputHeight = 2 * Math.Truncate(outputHeight / 2);
-
-                if (outputWidth != inputWidth || outputHeight != inputHeight)
-                {
-                    filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(UsCulture), outputHeight.ToString(UsCulture)));
-                }
-            }
-            else
-            {
-                // If fixed dimensions were supplied
-                if (request.Width.HasValue && request.Height.HasValue)
-                {
-                    var widthParam = request.Width.Value.ToString(UsCulture);
-                    var heightParam = request.Height.Value.ToString(UsCulture);
-
-                    filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
-                }
-
-                // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
-                else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
-                {
-                    var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
-                    var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
-
-                    filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam));
-                }
-
-                // If a fixed width was requested
-                else if (request.Width.HasValue)
-                {
-                    var widthParam = request.Width.Value.ToString(UsCulture);
-
-                    filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
-                }
-
-                // If a fixed height was requested
-                else if (request.Height.HasValue)
-                {
-                    var heightParam = request.Height.Value.ToString(UsCulture);
-
-                    filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam));
-                }
-
-                // If a max width was requested
-                else if (request.MaxWidth.HasValue)
-                {
-                    var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
-
-                    filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam));
-                }
-
-                // If a max height was requested
-                else if (request.MaxHeight.HasValue)
-                {
-                    var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
-
-                    filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam));
-                }
-            }
-
-            var output = string.Empty;
-
-            if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode)
-            {
-                var subParam = await GetTextSubtitleParam(state).ConfigureAwait(false);
-
-                filters.Add(subParam);
-
-                if (allowTimeStampCopy)
-                {
-                    output += " -copyts";
-                }
-            }
-
-            if (filters.Count > 0)
-            {
-                output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
-            }
-
-            return output;
-        }
-
-        /// <summary>
-        /// Gets the text subtitle param.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected async Task<string> GetTextSubtitleParam(EncodingJob state)
-        {
-            var seconds = Math.Round(TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds);
-
-            if (state.SubtitleStream.IsExternal)
-            {
-                var subtitlePath = state.SubtitleStream.Path;
-
-                var charsetParam = string.Empty;
-
-                if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
-                {
-                    var charenc = await SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).ConfigureAwait(false);
-
-                    if (!string.IsNullOrEmpty(charenc))
-                    {
-                        charsetParam = ":charenc=" + charenc;
-                    }
-                }
-
-                // TODO: Perhaps also use original_size=1920x800 ??
-                return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
-                    MediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
-                    charsetParam,
-                    seconds.ToString(UsCulture));
-            }
-
-            var mediaPath = state.MediaPath ?? string.Empty;
-
-            return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
-                MediaEncoder.EscapeSubtitleFilterPath(mediaPath),
-                state.InternalSubtitleStreamOffset.ToString(UsCulture),
-                seconds.ToString(UsCulture));
-        }
-
-        protected string GetAudioFilterParam(EncodingJob state, bool isHls)
-        {
-            var volParam = string.Empty;
-            var audioSampleRate = string.Empty;
-
-            var channels = state.OutputAudioChannels;
-
-            // Boost volume to 200% when downsampling from 6ch to 2ch
-            if (channels.HasValue && channels.Value <= 2)
-            {
-                if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !GetEncodingOptions().DownMixAudioBoost.Equals(1))
-                {
-                    volParam = ",volume=" + GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture);
-                }
-            }
-
-            if (state.OutputAudioSampleRate.HasValue)
-            {
-                audioSampleRate = state.OutputAudioSampleRate.Value + ":";
-            }
-
-            var adelay = isHls ? "adelay=1," : string.Empty;
-
-            var pts = string.Empty;
-
-            if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode && !state.Options.CopyTimestamps)
-            {
-                var seconds = TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds;
-
-                pts = string.Format(",asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(UsCulture));
-            }
-
-            return string.Format("-af \"{0}aresample={1}async={4}{2}{3}\"",
-
-                adelay,
-                audioSampleRate,
-                volParam,
-                pts,
-                state.OutputAudioSync);
-        }
     }
 }

+ 1681 - 0
MediaBrowser.MediaEncoding/Encoder/EncodingHelper.cs

@@ -0,0 +1,1681 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    public class EncodingHelper
+    {
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IServerConfigurationManager _config;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+
+        public EncodingHelper(IMediaEncoder mediaEncoder, IServerConfigurationManager config, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder)
+        {
+            _mediaEncoder = mediaEncoder;
+            _config = config;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+        }
+
+        public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+        {
+            var defaultEncoder = "libx264";
+
+            // Only use alternative encoders for video files.
+            // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+            // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+            if (state.VideoType == VideoType.VideoFile)
+            {
+                var hwType = encodingOptions.HardwareAccelerationType;
+
+                if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+                {
+                    return GetAvailableEncoder("h264_qsv", defaultEncoder);
+                }
+
+                if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    return GetAvailableEncoder("h264_nvenc", defaultEncoder);
+                }
+                if (string.Equals(hwType, "h264_omx", StringComparison.OrdinalIgnoreCase))
+                {
+                    return GetAvailableEncoder("h264_omx", defaultEncoder);
+                }
+                if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(encodingOptions.VaapiDevice))
+                {
+                    if (IsVaapiSupported(state))
+                    {
+                        return GetAvailableEncoder("h264_vaapi", defaultEncoder);
+                    }
+                }
+            }
+
+            return defaultEncoder;
+        }
+
+        private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder)
+        {
+            if (_mediaEncoder.SupportsEncoder(preferredEncoder))
+            {
+                return preferredEncoder;
+            }
+            return defaultEncoder;
+        }
+
+        private bool IsVaapiSupported(EncodingJobInfo state)
+        {
+            var videoStream = state.VideoStream;
+
+            if (videoStream != null)
+            {
+                // vaapi will throw an error with this input
+                // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99.
+                if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase))
+                {
+                    if (videoStream.Level == -99 || videoStream.Level == 15)
+                    {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        /// <summary>
+        /// Gets the name of the output video codec
+        /// </summary>
+        public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+        {
+            var codec = state.OutputVideoCodec;
+
+            if (!string.IsNullOrEmpty(codec))
+            {
+                if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+                {
+                    return GetH264Encoder(state, encodingOptions);
+                }
+                if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
+                {
+                    return "libvpx";
+                }
+                if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
+                {
+                    return "wmv2";
+                }
+                if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
+                {
+                    return "libtheora";
+                }
+
+                return codec.ToLower();
+            }
+
+            return "copy";
+        }
+
+        /// <summary>
+        /// Gets the user agent param.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        public string GetUserAgentParam(EncodingJobInfo state)
+        {
+            string useragent = null;
+
+            state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
+
+            if (!string.IsNullOrWhiteSpace(useragent))
+            {
+                return "-user-agent \"" + useragent + "\"";
+            }
+
+            return string.Empty;
+        }
+
+        public string GetInputFormat(string container)
+        {
+            if (string.Equals(container, "mkv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "matroska";
+            }
+            if (string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase))
+            {
+                return "mpegts";
+            }
+
+            return container;
+        }
+
+        public string GetDecoderFromCodec(string codec)
+        {
+            if (string.Equals(codec, "mp2", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+            if (string.Equals(codec, "aac_latm", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            return codec;
+        }
+
+        /// <summary>
+        /// Infers the audio codec based on the url
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <returns>System.Nullable{AudioCodecs}.</returns>
+        public string InferAudioCodec(string url)
+        {
+            var ext = Path.GetExtension(url);
+
+            if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase))
+            {
+                return "mp3";
+            }
+            if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase))
+            {
+                return "aac";
+            }
+            if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase))
+            {
+                return "wma";
+            }
+            if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase))
+            {
+                return "vorbis";
+            }
+            if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase))
+            {
+                return "vorbis";
+            }
+            if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "vorbis";
+            }
+            if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
+            {
+                return "vorbis";
+            }
+            if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase))
+            {
+                return "vorbis";
+            }
+
+            return "copy";
+        }
+
+        /// <summary>
+        /// Infers the video codec.
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <returns>System.Nullable{VideoCodecs}.</returns>
+        public string InferVideoCodec(string url)
+        {
+            var ext = Path.GetExtension(url);
+
+            if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
+            {
+                return "wmv";
+            }
+            if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
+            {
+                return "vpx";
+            }
+            if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "theora";
+            }
+            if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
+            {
+                return "h264";
+            }
+
+            return "copy";
+        }
+
+        public int GetVideoProfileScore(string profile)
+        {
+            var list = new List<string>
+            {
+                "Constrained Baseline",
+                "Baseline",
+                "Extended",
+                "Main",
+                "High",
+                "Progressive High",
+                "Constrained High"
+            };
+
+            return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase));
+        }
+
+        public string GetInputPathArgument(EncodingJobInfo state)
+        {
+            var protocol = state.InputProtocol;
+            var mediaPath = state.MediaPath ?? string.Empty;
+
+            var inputPath = new[] { mediaPath };
+
+            if (state.IsInputVideo)
+            {
+                if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
+                {
+                    inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
+                }
+            }
+
+            return _mediaEncoder.GetInputArgument(inputPath, protocol);
+        }
+
+        /// <summary>
+        /// Gets the audio encoder.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        public string GetAudioEncoder(EncodingJobInfo state)
+        {
+            var codec = state.OutputAudioCodec;
+
+            if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
+            {
+                return "aac -strict experimental";
+            }
+            if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
+            {
+                return "libmp3lame";
+            }
+            if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase))
+            {
+                return "libvorbis";
+            }
+            if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
+            {
+                return "wmav2";
+            }
+
+            return codec.ToLower();
+        }
+
+        /// <summary>
+        /// Gets the input argument.
+        /// </summary>
+        public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions)
+        {
+            var request = state.BaseRequest;
+
+            var arg = string.Format("-i {0}", GetInputPathArgument(state));
+
+            if (state.SubtitleStream != null && request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
+            {
+                if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
+                {
+                    if (state.VideoStream != null && state.VideoStream.Width.HasValue)
+                    {
+                        // This is hacky but not sure how to get the exact subtitle resolution
+                        double height = state.VideoStream.Width.Value;
+                        height /= 16;
+                        height *= 9;
+
+                        arg += string.Format(" -canvas_size {0}:{1}", state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture), Convert.ToInt32(height).ToString(CultureInfo.InvariantCulture));
+                    }
+
+                    var subtitlePath = state.SubtitleStream.Path;
+
+                    if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase))
+                    {
+                        var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
+                        if (_fileSystem.FileExists(idxFile))
+                        {
+                            subtitlePath = idxFile;
+                        }
+                    }
+
+                    arg += " -i \"" + subtitlePath + "\"";
+                }
+            }
+
+            if (state.IsVideoRequest)
+            {
+                if (GetVideoEncoder(state, encodingOptions).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && request.SubtitleMethod == SubtitleDeliveryMethod.Encode;
+                    var hwOutputFormat = "vaapi";
+
+                    if (hasGraphicalSubs)
+                    {
+                        hwOutputFormat = "yuv420p";
+                    }
+
+                    arg = "-hwaccel vaapi -hwaccel_output_format " + hwOutputFormat + " -vaapi_device " + encodingOptions.VaapiDevice + " " + arg;
+                }
+            }
+
+            return arg.Trim();
+        }
+
+        /// <summary>
+        /// Determines whether the specified stream is H264.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
+        public bool IsH264(MediaStream stream)
+        {
+            var codec = stream.Codec ?? string.Empty;
+
+            return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
+                   codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+        }
+
+        public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
+        {
+            var bitrate = state.OutputVideoBitrate;
+
+            if (bitrate.HasValue)
+            {
+                if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
+                {
+                    // With vpx when crf is used, b:v becomes a max rate
+                    // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
+                    return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(_usCulture));
+                }
+
+                if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+                {
+                    return string.Format(" -b:v {0}", bitrate.Value.ToString(_usCulture));
+                }
+
+                if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+                {
+                    // h264
+                    return string.Format(" -maxrate {0} -bufsize {1}",
+                        bitrate.Value.ToString(_usCulture),
+                        (bitrate.Value * 2).ToString(_usCulture));
+                }
+
+                // h264
+                return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}",
+                    bitrate.Value.ToString(_usCulture),
+                    (bitrate.Value * 2).ToString(_usCulture));
+            }
+
+            return string.Empty;
+        }
+
+        public string NormalizeTranscodingLevel(string videoCodec, string level)
+        {
+            double requestLevel;
+
+            // Clients may direct play higher than level 41, but there's no reason to transcode higher
+            if (double.TryParse(level, NumberStyles.Any, _usCulture, out requestLevel))
+            {
+                if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+                {
+                    if (requestLevel > 41)
+                    {
+                        return "41";
+                    }
+                }
+            }
+
+            return level;
+        }
+
+        /// <summary>
+        /// Gets the probe size argument.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        public string GetProbeSizeArgument(EncodingJobInfo state)
+        {
+            if (state.PlayableStreamFileNames.Count > 0)
+            {
+                return _mediaEncoder.GetProbeSizeAndAnalyzeDurationArgument(state.PlayableStreamFileNames.ToArray(), state.InputProtocol);
+            }
+            
+            return _mediaEncoder.GetProbeSizeAndAnalyzeDurationArgument(new[] { state.MediaPath }, state.InputProtocol);
+        }
+
+        /// <summary>
+        /// Gets the text subtitle param.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        public string GetTextSubtitleParam(EncodingJobInfo state)
+        {
+            var seconds = Math.Round(TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds);
+
+            var setPtsParam = state.CopyTimestamps
+                ? string.Empty
+                : string.Format(",setpts=PTS -{0}/TB", seconds.ToString(_usCulture));
+
+            if (state.SubtitleStream.IsExternal)
+            {
+                var subtitlePath = state.SubtitleStream.Path;
+
+                var charsetParam = string.Empty;
+
+                if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
+                {
+                    var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result;
+
+                    if (!string.IsNullOrEmpty(charenc))
+                    {
+                        charsetParam = ":charenc=" + charenc;
+                    }
+                }
+
+                // TODO: Perhaps also use original_size=1920x800 ??
+                return string.Format("subtitles=filename='{0}'{1}{2}",
+                    _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
+                    charsetParam,
+                    setPtsParam);
+            }
+
+            var mediaPath = state.MediaPath ?? string.Empty;
+
+            return string.Format("subtitles='{0}:si={1}'{2}",
+                _mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
+                state.InternalSubtitleStreamOffset.ToString(_usCulture),
+                setPtsParam);
+        }
+
+        public double? GetFramerateParam(EncodingJobInfo state)
+        {
+            var request = state.BaseRequest;
+
+            if (request.Framerate.HasValue)
+            {
+                return request.Framerate.Value;
+            }
+
+            var maxrate = request.MaxFramerate;
+
+            if (maxrate.HasValue && state.VideoStream != null)
+            {
+                var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
+
+                if (contentRate.HasValue && contentRate.Value > maxrate.Value)
+                {
+                    return maxrate;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the video bitrate to specify on the command line
+        /// </summary>
+        public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultH264Preset)
+        {
+            var param = string.Empty;
+
+            var isVc1 = state.VideoStream != null &&
+                string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
+
+            if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+            {
+                if (!string.IsNullOrWhiteSpace(encodingOptions.H264Preset))
+                {
+                    param += "-preset " + encodingOptions.H264Preset;
+                }
+                else
+                {
+                    param += "-preset " + defaultH264Preset;
+                }
+
+                if (encodingOptions.H264Crf >= 0 && encodingOptions.H264Crf <= 51)
+                {
+                    param += " -crf " + encodingOptions.H264Crf.ToString(CultureInfo.InvariantCulture);
+                }
+                else
+                {
+                    param += " -crf 23";
+                }
+            }
+
+            else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
+            {
+                param += "-preset fast";
+
+                param += " -crf 28";
+            }
+
+            // h264 (h264_qsv)
+            else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            {
+                param += "-preset 7 -look_ahead 0";
+
+            }
+
+            // h264 (h264_nvenc)
+            else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+            {
+                param += "-preset default";
+            }
+
+            // webm
+            else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase))
+            {
+                // Values 0-3, 0 being highest quality but slower
+                var profileScore = 0;
+
+                string crf;
+                var qmin = "0";
+                var qmax = "50";
+
+                crf = "10";
+
+                if (isVc1)
+                {
+                    profileScore++;
+                }
+
+                // Max of 2
+                profileScore = Math.Min(profileScore, 2);
+
+                // http://www.webmproject.org/docs/encoder-parameters/
+                param += string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+                    profileScore.ToString(_usCulture),
+                    crf,
+                    qmin,
+                    qmax);
+            }
+
+            else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
+            {
+                param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
+            }
+
+            // asf/wmv
+            else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase))
+            {
+                param += "-qmin 2";
+            }
+
+            else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+            {
+                param += "-mbd 2";
+            }
+
+            param += GetVideoBitrateParam(state, videoEncoder);
+
+            var framerate = GetFramerateParam(state);
+            if (framerate.HasValue)
+            {
+                param += string.Format(" -r {0}", framerate.Value.ToString(_usCulture));
+            }
+
+            if (!string.IsNullOrEmpty(state.OutputVideoSync))
+            {
+                param += " -vsync " + state.OutputVideoSync;
+            }
+
+            var request = state.BaseRequest;
+
+            if (!string.IsNullOrEmpty(request.Profile))
+            {
+                if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
+                    !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                {
+                    // not supported by h264_omx
+                    param += " -profile:v " + request.Profile;
+                }
+            }
+
+            if (!string.IsNullOrEmpty(request.Level))
+            {
+                var level = NormalizeTranscodingLevel(state.OutputVideoCodec, request.Level);
+
+                // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
+                // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
+                if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+                {
+                    switch (level)
+                    {
+                        case "30":
+                            param += " -level 3.0";
+                            break;
+                        case "31":
+                            param += " -level 3.1";
+                            break;
+                        case "32":
+                            param += " -level 3.2";
+                            break;
+                        case "40":
+                            param += " -level 4.0";
+                            break;
+                        case "41":
+                            param += " -level 4.1";
+                            break;
+                        case "42":
+                            param += " -level 4.2";
+                            break;
+                        case "50":
+                            param += " -level 5.0";
+                            break;
+                        case "51":
+                            param += " -level 5.1";
+                            break;
+                        case "52":
+                            param += " -level 5.2";
+                            break;
+                        default:
+                            param += " -level " + level;
+                            break;
+                    }
+                }
+                else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
+                {
+                    param += " -level " + level;
+                }
+            }
+
+            if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+            {
+                param += " -x264opts:0 subme=0:rc_lookahead=10:me_range=4:me=dia:no_chroma_me:8x8dct=0:partitions=none";
+            }
+
+            if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
+                !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) &&
+                !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            {
+                param = "-pix_fmt yuv420p " + param;
+            }
+
+            return param;
+        }
+
+        public bool CanStreamCopyVideo(EncodingJobInfo state, MediaStream videoStream)
+        {
+            var request = state.BaseRequest;
+
+            if (videoStream.IsInterlaced)
+            {
+                return false;
+            }
+
+            if (videoStream.IsAnamorphic ?? false)
+            {
+                return false;
+            }
+
+            // Can't stream copy if we're burning in subtitles
+            if (request.SubtitleStreamIndex.HasValue)
+            {
+                if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
+                {
+                    return false;
+                }
+            }
+
+            if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+            {
+                if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value && request.RequireAvc)
+                {
+                    return false;
+                }
+            }
+
+            // Source and target codecs must match
+            if (string.IsNullOrEmpty(videoStream.Codec) || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // If client is requesting a specific video profile, it must match the source
+            if (!string.IsNullOrEmpty(request.Profile))
+            {
+                if (string.IsNullOrEmpty(videoStream.Profile))
+                {
+                    //return false;
+                }
+
+                if (!string.IsNullOrEmpty(videoStream.Profile) && !string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
+                {
+                    var currentScore = GetVideoProfileScore(videoStream.Profile);
+                    var requestedScore = GetVideoProfileScore(request.Profile);
+
+                    if (currentScore == -1 || currentScore > requestedScore)
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            // Video width must fall within requested value
+            if (request.MaxWidth.HasValue)
+            {
+                if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Video height must fall within requested value
+            if (request.MaxHeight.HasValue)
+            {
+                if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Video framerate must fall within requested value
+            var requestedFramerate = request.MaxFramerate ?? request.Framerate;
+            if (requestedFramerate.HasValue)
+            {
+                var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate;
+
+                if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Video bitrate must fall within requested value
+            if (request.VideoBitRate.HasValue)
+            {
+                if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)
+                {
+                    return false;
+                }
+            }
+
+            if (request.MaxVideoBitDepth.HasValue)
+            {
+                if (videoStream.BitDepth.HasValue && videoStream.BitDepth.Value > request.MaxVideoBitDepth.Value)
+                {
+                    return false;
+                }
+            }
+
+            if (request.MaxRefFrames.HasValue)
+            {
+                if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > request.MaxRefFrames.Value)
+                {
+                    return false;
+                }
+            }
+
+            // If a specific level was requested, the source must match or be less than
+            if (!string.IsNullOrEmpty(request.Level))
+            {
+                double requestLevel;
+
+                if (double.TryParse(request.Level, NumberStyles.Any, _usCulture, out requestLevel))
+                {
+                    if (!videoStream.Level.HasValue)
+                    {
+                        //return false;
+                    }
+
+                    if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel)
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            return request.EnableAutoStreamCopy;
+        }
+
+        public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, List<string> supportedAudioCodecs)
+        {
+            var request = state.BaseRequest;
+
+            // Source and target codecs must match
+            if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // Video bitrate must fall within requested value
+            if (request.AudioBitRate.HasValue)
+            {
+                if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0)
+                {
+                    return false;
+                }
+                if (audioStream.BitRate.Value > request.AudioBitRate.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Channels must fall within requested value
+            var channels = request.AudioChannels ?? request.MaxAudioChannels;
+            if (channels.HasValue)
+            {
+                if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0)
+                {
+                    return false;
+                }
+                if (audioStream.Channels.Value > channels.Value)
+                {
+                    return false;
+                }
+            }
+
+            // Sample rate must fall within requested value
+            if (request.AudioSampleRate.HasValue)
+            {
+                if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0)
+                {
+                    return false;
+                }
+                if (audioStream.SampleRate.Value > request.AudioSampleRate.Value)
+                {
+                    return false;
+                }
+            }
+
+            return request.EnableAutoStreamCopy;
+        }
+
+        public int? GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
+        {
+            var bitrate = request.VideoBitRate;
+
+            if (videoStream != null)
+            {
+                var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue &&
+                                   request.Height.Value > videoStream.Height.Value;
+
+                if (request.Width.HasValue && videoStream.Width.HasValue &&
+                    request.Width.Value > videoStream.Width.Value)
+                {
+                    isUpscaling = true;
+                }
+
+                // Don't allow bitrate increases unless upscaling
+                if (!isUpscaling)
+                {
+                    if (bitrate.HasValue && videoStream.BitRate.HasValue)
+                    {
+                        bitrate = Math.Min(bitrate.Value, videoStream.BitRate.Value);
+                    }
+                }
+            }
+
+            if (bitrate.HasValue)
+            {
+                var inputVideoCodec = videoStream == null ? null : videoStream.Codec;
+                bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec);
+
+                // If a max bitrate was requested, don't let the scaled bitrate exceed it
+                if (request.VideoBitRate.HasValue)
+                {
+                    bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value);
+                }
+            }
+
+            return bitrate;
+        }
+
+        public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
+        {
+            if (request.AudioBitRate.HasValue)
+            {
+                // Make sure we don't request a bitrate higher than the source
+                var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
+
+                // Don't encode any higher than this
+                return Math.Min(384000, request.AudioBitRate.Value);
+                //return Math.Min(currentBitrate, request.AudioBitRate.Value);
+            }
+
+            return null;
+        }
+
+        public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
+        {
+            var volParam = string.Empty;
+            var audioSampleRate = string.Empty;
+
+            var channels = state.OutputAudioChannels;
+
+            // Boost volume to 200% when downsampling from 6ch to 2ch
+            if (channels.HasValue && channels.Value <= 2)
+            {
+                if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !encodingOptions.DownMixAudioBoost.Equals(1))
+                {
+                    volParam = ",volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture);
+                }
+            }
+
+            if (state.OutputAudioSampleRate.HasValue)
+            {
+                audioSampleRate = state.OutputAudioSampleRate.Value + ":";
+            }
+
+            var adelay = isHls ? "adelay=1," : string.Empty;
+
+            var pts = string.Empty;
+
+            if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.BaseRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode && !state.CopyTimestamps)
+            {
+                var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds;
+
+                pts = string.Format(",asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(_usCulture));
+            }
+
+            return string.Format("-af \"{0}aresample={1}async={4}{2}{3}\"",
+
+                adelay,
+                audioSampleRate,
+                volParam,
+                pts,
+                state.OutputAudioSync);
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="audioStream">The audio stream.</param>
+        /// <param name="outputAudioCodec">The output audio codec.</param>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        public int? GetNumAudioChannelsParam(BaseEncodingJobOptions request, MediaStream audioStream, string outputAudioCodec)
+        {
+            var inputChannels = audioStream == null
+                ? null
+                : audioStream.Channels;
+
+            if (inputChannels <= 0)
+            {
+                inputChannels = null;
+            }
+
+            int? transcoderChannelLimit = null;
+            var codec = outputAudioCodec ?? string.Empty;
+
+            if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                // wmav2 currently only supports two channel output
+                transcoderChannelLimit = 2;
+            }
+
+            else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                // libmp3lame currently only supports two channel output
+                transcoderChannelLimit = 2;
+            }
+            else
+            {
+                // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
+                transcoderChannelLimit = 6;
+            }
+
+            var isTranscodingAudio = !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
+
+            int? resultChannels = null;
+            if (isTranscodingAudio)
+            {
+                resultChannels = request.TranscodingMaxAudioChannels;
+            }
+            resultChannels = resultChannels ?? request.MaxAudioChannels ?? request.AudioChannels;
+
+            if (inputChannels.HasValue)
+            {
+                resultChannels = resultChannels.HasValue
+                    ? Math.Min(resultChannels.Value, inputChannels.Value)
+                    : inputChannels.Value;
+            }
+
+            if (isTranscodingAudio && transcoderChannelLimit.HasValue)
+            {
+                resultChannels = resultChannels.HasValue
+                    ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value)
+                    : transcoderChannelLimit.Value;
+            }
+
+            return resultChannels ?? request.AudioChannels;
+        }
+
+        /// <summary>
+        /// Enforces the resolution limit.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        public void EnforceResolutionLimit(EncodingJobInfo state)
+        {
+            var videoRequest = state.BaseRequest;
+
+            // Switch the incoming params to be ceilings rather than fixed values
+            videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
+            videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
+
+            videoRequest.Width = null;
+            videoRequest.Height = null;
+        }
+
+        /// <summary>
+        /// Gets the fast seek command line parameter.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.String.</returns>
+        /// <value>The fast seek command line parameter.</value>
+        public string GetFastSeekCommandLineParameter(BaseEncodingJobOptions request)
+        {
+            var time = request.StartTimeTicks ?? 0;
+
+            if (time > 0)
+            {
+                return string.Format("-ss {0}", _mediaEncoder.GetTimeParameter(time));
+            }
+
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Gets the map args.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        public string GetMapArgs(EncodingJobInfo state)
+        {
+            // If we don't have known media info
+            // If input is video, use -sn to drop subtitles
+            // Otherwise just return empty
+            if (state.VideoStream == null && state.AudioStream == null)
+            {
+                return state.IsInputVideo ? "-sn" : string.Empty;
+            }
+
+            // We have media info, but we don't know the stream indexes
+            if (state.VideoStream != null && state.VideoStream.Index == -1)
+            {
+                return "-sn";
+            }
+
+            // We have media info, but we don't know the stream indexes
+            if (state.AudioStream != null && state.AudioStream.Index == -1)
+            {
+                return state.IsInputVideo ? "-sn" : string.Empty;
+            }
+
+            var args = string.Empty;
+
+            if (state.VideoStream != null)
+            {
+                args += string.Format("-map 0:{0}", state.VideoStream.Index);
+            }
+            else
+            {
+                // No known video stream
+                args += "-vn";
+            }
+
+            if (state.AudioStream != null)
+            {
+                args += string.Format(" -map 0:{0}", state.AudioStream.Index);
+            }
+
+            else
+            {
+                args += " -map -0:a";
+            }
+
+            var subtitleMethod = state.BaseRequest.SubtitleMethod;
+            if (state.SubtitleStream == null || subtitleMethod == SubtitleDeliveryMethod.Hls)
+            {
+                args += " -map -0:s";
+            }
+            else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
+            {
+                args += string.Format(" -map 0:{0}", state.SubtitleStream.Index);
+            }
+            else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
+            {
+                args += " -map 1:0 -sn";
+            }
+
+            return args;
+        }
+
+        /// <summary>
+        /// Determines which stream will be used for playback
+        /// </summary>
+        /// <param name="allStream">All stream.</param>
+        /// <param name="desiredIndex">Index of the desired.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param>
+        /// <returns>MediaStream.</returns>
+        public MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
+        {
+            var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList();
+
+            if (desiredIndex.HasValue)
+            {
+                var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
+
+                if (stream != null)
+                {
+                    return stream;
+                }
+            }
+
+            if (type == MediaStreamType.Video)
+            {
+                streams = streams.Where(i => !string.Equals(i.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList();
+            }
+
+            if (returnFirstIfNoIndex && type == MediaStreamType.Audio)
+            {
+                return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ??
+                       streams.FirstOrDefault();
+            }
+
+            // Just return the first one
+            return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
+        }
+
+        /// <summary>
+        /// Gets the internal graphical subtitle param.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="outputVideoCodec">The output video codec.</param>
+        /// <returns>System.String.</returns>
+        public string GetGraphicalSubtitleParam(EncodingJobInfo state, string outputVideoCodec)
+        {
+            var outputSizeParam = string.Empty;
+
+            var request = state.BaseRequest;
+
+            // Add resolution params, if specified
+            if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
+            {
+                outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
+
+                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                {
+                    outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase));
+                }
+                else
+                {
+                    outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
+                }
+            }
+
+            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && outputSizeParam.Length == 0)
+            {
+                outputSizeParam = ",format=nv12|vaapi,hwupload";
+            }
+
+            var videoSizeParam = string.Empty;
+            
+            if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
+            {
+                videoSizeParam = string.Format("scale={0}:{1}", state.VideoStream.Width.Value.ToString(_usCulture), state.VideoStream.Height.Value.ToString(_usCulture));
+            }
+
+            var mapPrefix = state.SubtitleStream.IsExternal ?
+                1 :
+                0;
+
+            var subtitleStreamIndex = state.SubtitleStream.IsExternal
+                ? 0
+                : state.SubtitleStream.Index;
+
+            return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub] ; [0:{2}] [sub] overlay{3}\"",
+                mapPrefix.ToString(_usCulture),
+                subtitleStreamIndex.ToString(_usCulture),
+                state.VideoStream.Index.ToString(_usCulture),
+                outputSizeParam,
+                videoSizeParam);
+        }
+
+        /// <summary>
+        /// If we're going to put a fixed size on the command line, this will calculate it
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="outputVideoCodec">The output video codec.</param>
+        /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
+        /// <returns>System.String.</returns>
+        public string GetOutputSizeParam(EncodingJobInfo state,
+            string outputVideoCodec,
+            bool allowTimeStampCopy = true)
+        {
+            // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
+
+            var request = state.BaseRequest;
+
+            var filters = new List<string>();
+
+            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            {
+                filters.Add("format=nv12|vaapi");
+                filters.Add("hwupload");
+            }
+            else if (state.DeInterlace && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            {
+                filters.Add("yadif=0:-1:0");
+            }
+
+            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            {
+                // Work around vaapi's reduced scaling features
+                var scaler = "scale_vaapi";
+
+                // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions
+                // (outputWidth, outputHeight). The user may request precise output dimensions or maximum
+                // output dimensions. Output dimensions are guaranteed to be even.
+                decimal inputWidth = Convert.ToDecimal(state.VideoStream.Width);
+                decimal inputHeight = Convert.ToDecimal(state.VideoStream.Height);
+                decimal outputWidth = request.Width.HasValue ? Convert.ToDecimal(request.Width.Value) : inputWidth;
+                decimal outputHeight = request.Height.HasValue ? Convert.ToDecimal(request.Height.Value) : inputHeight;
+                decimal maximumWidth = request.MaxWidth.HasValue ? Convert.ToDecimal(request.MaxWidth.Value) : outputWidth;
+                decimal maximumHeight = request.MaxHeight.HasValue ? Convert.ToDecimal(request.MaxHeight.Value) : outputHeight;
+
+                if (outputWidth > maximumWidth || outputHeight > maximumHeight)
+                {
+                    var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight);
+                    outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale));
+                    outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale));
+                }
+
+                outputWidth = 2 * Math.Truncate(outputWidth / 2);
+                outputHeight = 2 * Math.Truncate(outputHeight / 2);
+
+                if (outputWidth != inputWidth || outputHeight != inputHeight)
+                {
+                    filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(_usCulture), outputHeight.ToString(_usCulture)));
+                }
+            }
+            else
+            {
+                // If fixed dimensions were supplied
+                if (request.Width.HasValue && request.Height.HasValue)
+                {
+                    var widthParam = request.Width.Value.ToString(_usCulture);
+                    var heightParam = request.Height.Value.ToString(_usCulture);
+
+                    filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
+                }
+
+                // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
+                else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
+                {
+                    var maxWidthParam = request.MaxWidth.Value.ToString(_usCulture);
+                    var maxHeightParam = request.MaxHeight.Value.ToString(_usCulture);
+
+                    filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam));
+                }
+
+                // If a fixed width was requested
+                else if (request.Width.HasValue)
+                {
+                    var widthParam = request.Width.Value.ToString(_usCulture);
+
+                    filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
+                }
+
+                // If a fixed height was requested
+                else if (request.Height.HasValue)
+                {
+                    var heightParam = request.Height.Value.ToString(_usCulture);
+
+                    filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam));
+                }
+
+                // If a max width was requested
+                else if (request.MaxWidth.HasValue)
+                {
+                    var maxWidthParam = request.MaxWidth.Value.ToString(_usCulture);
+
+                    filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam));
+                }
+
+                // If a max height was requested
+                else if (request.MaxHeight.HasValue)
+                {
+                    var maxHeightParam = request.MaxHeight.Value.ToString(_usCulture);
+
+                    filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam));
+                }
+            }
+
+            var output = string.Empty;
+
+            if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
+            {
+                var subParam = GetTextSubtitleParam(state);
+
+                filters.Add(subParam);
+
+                if (allowTimeStampCopy)
+                {
+                    output += " -copyts";
+                }
+            }
+
+            if (filters.Count > 0)
+            {
+                output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
+            }
+
+            return output;
+        }
+
+
+        /// <summary>
+        /// Gets the number of threads.
+        /// </summary>
+        /// <returns>System.Int32.</returns>
+        public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, bool isWebm)
+        {
+            var threads = GetNumberOfThreadsInternal(state, encodingOptions, isWebm);
+
+            if (state.BaseRequest.CpuCoreLimit.HasValue && state.BaseRequest.CpuCoreLimit.Value > 0)
+            {
+                threads = Math.Min(threads, state.BaseRequest.CpuCoreLimit.Value);
+            }
+
+            return threads;
+        }
+
+        public void TryStreamCopy(EncodingJobInfo state)
+        {
+            if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream))
+            {
+                state.OutputVideoCodec = "copy";
+            }
+            else
+            {
+                var user = state.User;
+
+                // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
+                if (user != null && !user.Policy.EnableVideoPlaybackTranscoding)
+                {
+                    state.OutputVideoCodec = "copy";
+                }
+            }
+
+            if (state.AudioStream != null && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs))
+            {
+                state.OutputAudioCodec = "copy";
+            }
+            else
+            {
+                var user = state.User;
+
+                // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
+                if (user != null && !user.Policy.EnableAudioPlaybackTranscoding)
+                {
+                    state.OutputAudioCodec = "copy";
+                }
+            }
+        }
+
+        public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions)
+        {
+            var inputModifier = string.Empty;
+
+            var probeSize = GetProbeSizeArgument(state);
+            inputModifier += " " + probeSize;
+            inputModifier = inputModifier.Trim();
+
+            var userAgentParam = GetUserAgentParam(state);
+
+            if (!string.IsNullOrWhiteSpace(userAgentParam))
+            {
+                inputModifier += " " + userAgentParam;
+            }
+
+            inputModifier = inputModifier.Trim();
+
+            inputModifier += " " + GetFastSeekCommandLineParameter(state.BaseRequest);
+            inputModifier = inputModifier.Trim();
+
+            //inputModifier += " -fflags +genpts+ignidx+igndts";
+            //if (state.IsVideoRequest && genPts)
+            //{
+            //    inputModifier += " -fflags +genpts";
+            //}
+
+            if (!string.IsNullOrEmpty(state.InputAudioSync))
+            {
+                inputModifier += " -async " + state.InputAudioSync;
+            }
+
+            if (!string.IsNullOrEmpty(state.InputVideoSync))
+            {
+                inputModifier += " -vsync " + state.InputVideoSync;
+            }
+
+            if (state.ReadInputAtNativeFramerate)
+            {
+                inputModifier += " -re";
+            }
+
+            var videoDecoder = GetVideoDecoder(state, encodingOptions);
+            if (!string.IsNullOrWhiteSpace(videoDecoder))
+            {
+                inputModifier += " " + videoDecoder;
+            }
+
+            if (state.IsVideoRequest)
+            {
+                // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
+                if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase) && state.CopyTimestamps)
+                {
+                    //inputModifier += " -noaccurate_seek";
+                }
+
+                //if (!string.IsNullOrWhiteSpace(state.InputContainer))
+                //{
+                //    var inputFormat = GetInputFormat(state.InputContainer);
+                //    if (!string.IsNullOrWhiteSpace(inputFormat))
+                //    {
+                //        inputModifier += " -f " + inputFormat;
+                //    }
+                //}
+
+                //if (state.RunTimeTicks.HasValue)
+                //{
+                //    foreach (var stream in state.MediaSource.MediaStreams)
+                //    {
+                //        if (!stream.IsExternal && stream.Type != MediaStreamType.Subtitle)
+                //        {
+                //            if (!string.IsNullOrWhiteSpace(stream.Codec) && stream.Index != -1)
+                //            {
+                //                var decoder = GetDecoderFromCodec(stream.Codec);
+
+                //                if (!string.IsNullOrWhiteSpace(decoder))
+                //                {
+                //                    inputModifier += " -codec:" + stream.Index.ToString(_usCulture) + " " + decoder;
+                //                }
+                //            }
+                //        }
+                //    }
+                //}
+            }
+
+            return inputModifier;
+        }
+
+
+        public void AttachMediaSourceInfo(EncodingJobInfo state,
+          MediaSourceInfo mediaSource,
+          string requestedUrl)
+        {
+            state.MediaPath = mediaSource.Path;
+            state.InputProtocol = mediaSource.Protocol;
+            state.InputContainer = mediaSource.Container;
+            state.RunTimeTicks = mediaSource.RunTimeTicks;
+            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+
+            if (mediaSource.VideoType.HasValue)
+            {
+                state.VideoType = mediaSource.VideoType.Value;
+            }
+
+            state.IsoType = mediaSource.IsoType;
+
+            state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
+
+            if (mediaSource.Timestamp.HasValue)
+            {
+                state.InputTimestamp = mediaSource.Timestamp.Value;
+            }
+
+            state.InputProtocol = mediaSource.Protocol;
+            state.MediaPath = mediaSource.Path;
+            state.RunTimeTicks = mediaSource.RunTimeTicks;
+            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+
+            if (state.ReadInputAtNativeFramerate ||
+                mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
+            {
+                state.OutputAudioSync = "1000";
+                state.InputVideoSync = "-1";
+                state.InputAudioSync = "1";
+            }
+
+            if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase))
+            {
+                // Seeing some stuttering when transcoding wma to audio-only HLS
+                state.InputAudioSync = "1";
+            }
+
+            var mediaStreams = mediaSource.MediaStreams;
+
+            if (state.IsVideoRequest)
+            {
+                var videoRequest = state.BaseRequest;
+
+                if (string.IsNullOrEmpty(videoRequest.VideoCodec))
+                {
+                    if (string.IsNullOrWhiteSpace(requestedUrl))
+                    {
+                        requestedUrl = "test." + videoRequest.OutputContainer;
+                    }
+
+                    videoRequest.VideoCodec = InferVideoCodec(requestedUrl);
+                }
+
+                state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
+                state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
+                state.SubtitleDeliveryMethod = videoRequest.SubtitleMethod;
+                state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
+
+                if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal)
+                {
+                    state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream);
+                }
+
+                if (state.VideoStream != null && state.VideoStream.IsInterlaced)
+                {
+                    state.DeInterlace = true;
+                }
+
+                EnforceResolutionLimit(state);
+            }
+            else
+            {
+                state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
+            }
+
+            state.MediaSource = mediaSource;
+        }
+
+        /// <summary>
+        /// Gets the name of the output video codec
+        /// </summary>
+        protected string GetVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+        {
+            if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            // Only use alternative encoders for video files.
+            // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+            // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+            if (state.VideoType != VideoType.VideoFile)
+            {
+                return null;
+            }
+
+            if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
+            {
+                if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+                {
+                    switch (state.MediaSource.VideoStream.Codec.ToLower())
+                    {
+                        case "avc":
+                        case "h264":
+                            if (_mediaEncoder.SupportsDecoder("h264_qsv"))
+                            {
+                                return "-c:v h264_qsv ";
+                            }
+                            break;
+                        case "mpeg2video":
+                            if (_mediaEncoder.SupportsDecoder("mpeg2_qsv"))
+                            {
+                                return "-c:v mpeg2_qsv ";
+                            }
+                            break;
+                        case "vc1":
+                            if (_mediaEncoder.SupportsDecoder("vc1_qsv"))
+                            {
+                                return "-c:v vc1_qsv ";
+                            }
+                            break;
+                    }
+                }
+            }
+
+            // leave blank so ffmpeg will decide
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the number of threads.
+        /// </summary>
+        /// <returns>System.Int32.</returns>
+        private int GetNumberOfThreadsInternal(EncodingJobInfo state, EncodingOptions encodingOptions, bool isWebm)
+        {
+            var threads = encodingOptions.EncodingThreadCount;
+
+            if (isWebm)
+            {
+                // Recommended per docs
+                return Math.Max(Environment.ProcessorCount - 1, 2);
+            }
+
+            // Automatic
+            if (threads == -1)
+            {
+                return 0;
+            }
+
+            return threads;
+        }
+    }
+}

+ 10 - 73
MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs

@@ -17,7 +17,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.MediaEncoding.Encoder
 {
-    public class EncodingJob : IDisposable
+    public class EncodingJob : EncodingJobInfo, IDisposable
     {
         public bool HasExited { get; internal set; }
         public bool IsCancelled { get; internal set; }
@@ -25,46 +25,24 @@ namespace MediaBrowser.MediaEncoding.Encoder
         public Stream LogFileStream { get; set; }
         public IProgress<double> Progress { get; set; }
         public TaskCompletionSource<bool> TaskCompletionSource;
-        public EncodingJobOptions Options { get; set; }
-        public string InputContainer { get; set; }
-        public MediaSourceInfo MediaSource { get; set; }
-        public MediaStream AudioStream { get; set; }
-        public MediaStream VideoStream { get; set; }
-        public MediaStream SubtitleStream { get; set; }
-        public IIsoMount IsoMount { get; set; }
-
-        public bool ReadInputAtNativeFramerate { get; set; }
-        public bool IsVideoRequest { get; set; }
-        public string InputAudioSync { get; set; }
-        public string InputVideoSync { get; set; }
-        public string Id { get; set; }
 
-        public string MediaPath { get; set; }
-        public MediaProtocol InputProtocol { get; set; }
-        public bool IsInputVideo { get; set; }
-        public VideoType VideoType { get; set; }
-        public IsoType? IsoType { get; set; }
-        public List<string> PlayableStreamFileNames { get; set; }
+        public EncodingJobOptions Options
+        {
+            get { return (EncodingJobOptions) BaseRequest; }
+            set { BaseRequest = value; }
+        }
 
-        public List<string> SupportedAudioCodecs { get; set; }
-        public Dictionary<string, string> RemoteHttpHeaders { get; set; }
-        public TransportStreamTimestamp InputTimestamp { get; set; }
+        public string Id { get; set; }
 
-        public bool DeInterlace { get; set; }
         public string MimeType { get; set; }
         public bool EstimateContentLength { get; set; }
         public bool EnableMpegtsM2TsMode { get; set; }
         public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
         public long? EncodingDurationTicks { get; set; }
         public string LiveStreamId { get; set; }
-        public long? RunTimeTicks;
 
         public string ItemType { get; set; }
 
-        public long? InputBitrate { get; set; }
-        public long? InputFileSize { get; set; }
-        public string OutputAudioSync = "1";
-        public string OutputVideoSync = "vfr";
         public string AlbumCoverPath { get; set; }
 
         public string GetMimeType(string outputPath)
@@ -80,17 +58,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
         private readonly ILogger _logger;
         private readonly IMediaSourceManager _mediaSourceManager;
 
-        public EncodingJob(ILogger logger, IMediaSourceManager mediaSourceManager)
+        public EncodingJob(ILogger logger, IMediaSourceManager mediaSourceManager) : 
+            base(logger)
         {
             _logger = logger;
             _mediaSourceManager = mediaSourceManager;
             Id = Guid.NewGuid().ToString("N");
 
-            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             _logger = logger;
-            SupportedAudioCodecs = new List<string>();
-            PlayableStreamFileNames = new List<string>();
-            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             TaskCompletionSource = new TaskCompletionSource<bool>();
         }
 
@@ -118,23 +93,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
         }
 
-        private void DisposeIsoMount()
-        {
-            if (IsoMount != null)
-            {
-                try
-                {
-                    IsoMount.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error disposing iso mount", ex);
-                }
-
-                IsoMount = null;
-            }
-        }
-
         private async void DisposeLiveStream()
         {
             if (MediaSource.RequiresClosing)
@@ -150,15 +108,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
         }
 
-        public int InternalSubtitleStreamOffset { get; set; }
-
         public string OutputFilePath { get; set; }
-        public string OutputVideoCodec { get; set; }
-        public string OutputAudioCodec { get; set; }
-        public int? OutputAudioChannels;
-        public int? OutputAudioSampleRate;
         public int? OutputAudioBitrate;
-        public int? OutputVideoBitrate;
 
         public string ActualOutputVideoCodec
         {
@@ -313,25 +264,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
         }
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream
-        /// </summary>
-        public double? TargetVideoLevel
-        {
-            get
-            {
-                var stream = VideoStream;
-                return Options.Level.HasValue && !Options.Static
-                    ? Options.Level.Value
-                    : stream == null ? null : stream.Level;
-            }
-        }
-
         public TransportStreamTimestamp TargetTimestamp
         {
             get
             {
-                var defaultValue = string.Equals(Options.OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ?
+                var defaultValue = string.Equals(OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ?
                     TransportStreamTimestamp.Valid :
                     TransportStreamTimestamp.None;
 

+ 12 - 606
MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs

@@ -33,7 +33,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             _config = config;
         }
 
-        public async Task<EncodingJob> CreateJob(EncodingJobOptions options, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
+        public async Task<EncodingJob> CreateJob(EncodingJobOptions options, EncodingHelper encodingHelper, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
         {
             var request = options;
 
@@ -49,6 +49,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 Progress = progress
             };
 
+            if (!string.IsNullOrWhiteSpace(request.VideoCodec))
+            {
+                state.SupportedVideoCodecs = request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+                request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+            }
+
             if (!string.IsNullOrWhiteSpace(request.AudioCodec))
             {
                 state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
@@ -76,7 +82,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             var videoRequest = state.Options;
 
-            AttachMediaSourceInfo(state, mediaSource, videoRequest);
+            encodingHelper.AttachMediaSourceInfo(state, mediaSource, null);
 
             //var container = Path.GetExtension(state.RequestedUrl);
 
@@ -89,17 +95,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             //state.OutputContainer = (container ?? string.Empty).TrimStart('.');
 
-            state.OutputAudioBitrate = GetAudioBitrateParam(state.Options, state.AudioStream);
+            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.Options, state.AudioStream);
             state.OutputAudioSampleRate = request.AudioSampleRate;
 
             state.OutputAudioCodec = state.Options.AudioCodec;
 
-            state.OutputAudioChannels = GetNumAudioChannelsParam(state.Options, state.AudioStream, state.OutputAudioCodec);
+            state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state.Options, state.AudioStream, state.OutputAudioCodec);
 
             if (videoRequest != null)
             {
                 state.OutputVideoCodec = state.Options.VideoCodec;
-                state.OutputVideoBitrate = GetVideoBitrateParamValue(state.Options, state.VideoStream, state.OutputVideoCodec);
+                state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.Options, state.VideoStream, state.OutputVideoCodec);
 
                 if (state.OutputVideoBitrate.HasValue)
                 {
@@ -120,7 +126,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             if (videoRequest != null)
             {
-                TryStreamCopy(state, videoRequest);
+                encodingHelper.TryStreamCopy(state);
             }
 
             //state.OutputFilePath = GetOutputFilePath(state);
@@ -128,104 +134,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return state;
         }
 
-        internal static void TryStreamCopy(EncodingJob state,
-            EncodingJobOptions videoRequest)
-        {
-            if (state.IsVideoRequest)
-            {
-                if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
-                {
-                    state.OutputVideoCodec = "copy";
-                }
-
-                if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
-                {
-                    state.OutputAudioCodec = "copy";
-                }
-            }
-        }
-
-        internal static void AttachMediaSourceInfo(EncodingJob state,
-            MediaSourceInfo mediaSource,
-            EncodingJobOptions videoRequest)
-        {
-            state.MediaPath = mediaSource.Path;
-            state.InputProtocol = mediaSource.Protocol;
-            state.InputContainer = mediaSource.Container;
-            state.InputFileSize = mediaSource.Size;
-            state.InputBitrate = mediaSource.Bitrate;
-            state.RunTimeTicks = mediaSource.RunTimeTicks;
-            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
-
-            if (mediaSource.VideoType.HasValue)
-            {
-                state.VideoType = mediaSource.VideoType.Value;
-            }
-
-            state.IsoType = mediaSource.IsoType;
-
-            state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
-
-            if (mediaSource.Timestamp.HasValue)
-            {
-                state.InputTimestamp = mediaSource.Timestamp.Value;
-            }
-
-            state.InputProtocol = mediaSource.Protocol;
-            state.MediaPath = mediaSource.Path;
-            state.RunTimeTicks = mediaSource.RunTimeTicks;
-            state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
-            state.InputBitrate = mediaSource.Bitrate;
-            state.InputFileSize = mediaSource.Size;
-            state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
-
-            if (state.ReadInputAtNativeFramerate ||
-                mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
-            {
-                state.OutputAudioSync = "1000";
-                state.InputVideoSync = "-1";
-                state.InputAudioSync = "1";
-            }
-
-            if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase))
-            {
-                // Seeing some stuttering when transcoding wma to audio-only HLS
-                state.InputAudioSync = "1";
-            }
-
-            var mediaStreams = mediaSource.MediaStreams;
-
-            if (videoRequest != null)
-            {
-                if (string.IsNullOrEmpty(videoRequest.VideoCodec))
-                {
-                    videoRequest.VideoCodec = InferVideoCodec(videoRequest.OutputContainer);
-                }
-
-                state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
-                state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
-                state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
-
-                if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal)
-                {
-                    state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream);
-                }
-
-                if (state.VideoStream != null && state.VideoStream.IsInterlaced)
-                {
-                    state.DeInterlace = true;
-                }
-
-                EnforceResolutionLimit(state, videoRequest);
-            }
-            else
-            {
-                state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
-            }
-
-            state.MediaSource = mediaSource;
-        }
-
         protected EncodingOptions GetEncodingOptions()
         {
             return _config.GetConfiguration<EncodingOptions>("encoding");
@@ -300,203 +208,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return "copy";
         }
 
-        /// <summary>
-        /// Determines which stream will be used for playback
-        /// </summary>
-        /// <param name="allStream">All stream.</param>
-        /// <param name="desiredIndex">Index of the desired.</param>
-        /// <param name="type">The type.</param>
-        /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param>
-        /// <returns>MediaStream.</returns>
-        private static MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
-        {
-            var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList();
-
-            if (desiredIndex.HasValue)
-            {
-                var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
-
-                if (stream != null)
-                {
-                    return stream;
-                }
-            }
-
-            if (type == MediaStreamType.Video)
-            {
-                streams = streams.Where(i => !string.Equals(i.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-
-            if (returnFirstIfNoIndex && type == MediaStreamType.Audio)
-            {
-                return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ??
-                       streams.FirstOrDefault();
-            }
-
-            // Just return the first one
-            return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
-        }
-
-        /// <summary>
-        /// Enforces the resolution limit.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="videoRequest">The video request.</param>
-        private static void EnforceResolutionLimit(EncodingJob state, EncodingJobOptions videoRequest)
-        {
-            // Switch the incoming params to be ceilings rather than fixed values
-            videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
-            videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
-
-            videoRequest.Width = null;
-            videoRequest.Height = null;
-        }
-
-        /// <summary>
-        /// Gets the number of audio channels to specify on the command line
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="audioStream">The audio stream.</param>
-        /// <param name="outputAudioCodec">The output audio codec.</param>
-        /// <returns>System.Nullable{System.Int32}.</returns>
-        private int? GetNumAudioChannelsParam(EncodingJobOptions request, MediaStream audioStream, string outputAudioCodec)
-        {
-            var inputChannels = audioStream == null
-                ? null
-                : audioStream.Channels;
-
-            if (inputChannels <= 0)
-            {
-                inputChannels = null;
-            }
-
-            int? transcoderChannelLimit = null;
-            var codec = outputAudioCodec ?? string.Empty;
-
-            if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                // wmav2 currently only supports two channel output
-                transcoderChannelLimit = 2;
-            }
-
-            else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                // libmp3lame currently only supports two channel output
-                transcoderChannelLimit = 2;
-            }
-            else
-            {
-                // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
-                transcoderChannelLimit = 6;
-            }
-
-            var isTranscodingAudio = !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
-
-            int? resultChannels = null;
-            if (isTranscodingAudio)
-            {
-                resultChannels = request.TranscodingMaxAudioChannels;
-            }
-            resultChannels = resultChannels ?? request.MaxAudioChannels ?? request.AudioChannels;
-
-            if (inputChannels.HasValue)
-            {
-                resultChannels = resultChannels.HasValue
-                    ? Math.Min(resultChannels.Value, inputChannels.Value)
-                    : inputChannels.Value;
-            }
-
-            if (isTranscodingAudio && transcoderChannelLimit.HasValue)
-            {
-                resultChannels = resultChannels.HasValue
-                    ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value)
-                    : transcoderChannelLimit.Value;
-            }
-
-            return resultChannels ?? request.AudioChannels;
-        }
-
-        private int? GetVideoBitrateParamValue(EncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
-        {
-            var bitrate = request.VideoBitRate;
-
-            if (videoStream != null)
-            {
-                var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue &&
-                                   request.Height.Value > videoStream.Height.Value;
-
-                if (request.Width.HasValue && videoStream.Width.HasValue &&
-                    request.Width.Value > videoStream.Width.Value)
-                {
-                    isUpscaling = true;
-                }
-
-                // Don't allow bitrate increases unless upscaling
-                if (!isUpscaling)
-                {
-                    if (bitrate.HasValue && videoStream.BitRate.HasValue)
-                    {
-                        bitrate = Math.Min(bitrate.Value, videoStream.BitRate.Value);
-                    }
-                }
-            }
-
-            if (bitrate.HasValue)
-            {
-                var inputVideoCodec = videoStream == null ? null : videoStream.Codec;
-                bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec);
-
-                // If a max bitrate was requested, don't let the scaled bitrate exceed it
-                if (request.VideoBitRate.HasValue)
-                {
-                    bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value);
-                }
-            }
-
-            return bitrate;
-        }
-
-        protected string GetVideoBitrateParam(EncodingJob state, string videoCodec, bool isHls)
-        {
-            var bitrate = state.OutputVideoBitrate;
-
-            if (bitrate.HasValue)
-            {
-                if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
-                {
-                    // With vpx when crf is used, b:v becomes a max rate
-                    // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
-                    return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
-                }
-
-                if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
-                {
-                    return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
-                }
-
-                // h264
-                return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}",
-                    bitrate.Value.ToString(UsCulture),
-                    (bitrate.Value * 2).ToString(UsCulture));
-            }
-
-            return string.Empty;
-        }
-
-        private int? GetAudioBitrateParam(EncodingJobOptions request, MediaStream audioStream)
-        {
-            if (request.AudioBitRate.HasValue)
-            {
-                // Make sure we don't request a bitrate higher than the source
-                var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
-
-                return request.AudioBitRate.Value;
-                //return Math.Min(currentBitrate, request.AudioBitRate.Value);
-            }
-
-            return null;
-        }
-
         /// <summary>
         /// Determines whether the specified stream is H264.
         /// </summary>
@@ -510,260 +221,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
                    codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
         }
 
-        /// <summary>
-        /// Gets the name of the output audio codec
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        internal static string GetAudioEncoder(EncodingJob state)
-        {
-            var codec = state.OutputAudioCodec;
-
-            if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
-            {
-                return "aac -strict experimental";
-            }
-            if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
-            {
-                return "libmp3lame";
-            }
-            if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase))
-            {
-                return "libvorbis";
-            }
-            if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
-            {
-                return "wmav2";
-            }
-
-            return codec.ToLower();
-        }
-
-        /// <summary>
-        /// Gets the name of the output video codec
-        /// </summary>
-        /// <returns>System.String.</returns>
-        internal static string GetVideoEncoder(IMediaEncoder mediaEncoder, EncodingJob state, EncodingOptions options)
-        {
-            var codec = state.OutputVideoCodec;
-
-            if (!string.IsNullOrEmpty(codec))
-            {
-                if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetH264Encoder(mediaEncoder, state, options);
-                }
-                if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "libvpx";
-                }
-                if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "wmv2";
-                }
-                if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
-                {
-                    return "libtheora";
-                }
-
-                return codec.ToLower();
-            }
-
-            return "copy";
-        }
-
-        private static string GetAvailableEncoder(IMediaEncoder mediaEncoder, string preferredEncoder, string defaultEncoder)
-        {
-            if (mediaEncoder.SupportsEncoder(preferredEncoder))
-            {
-                return preferredEncoder;
-            }
-            return defaultEncoder;
-        }
-
-        internal static string GetH264Encoder(IMediaEncoder mediaEncoder, EncodingJob state, EncodingOptions options)
-        {
-            var defaultEncoder = "libx264";
-
-            // Only use alternative encoders for video files.
-            // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
-            // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
-            if (state.VideoType == VideoType.VideoFile)
-            {
-                var hwType = options.HardwareAccelerationType;
-
-                if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetAvailableEncoder(mediaEncoder, "h264_qsv", defaultEncoder);
-                }
-
-                if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetAvailableEncoder(mediaEncoder, "h264_nvenc", defaultEncoder);
-                }
-                if (string.Equals(hwType, "h264_omx", StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetAvailableEncoder(mediaEncoder, "h264_omx", defaultEncoder);
-                }
-                if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(options.VaapiDevice))
-                {
-                    if (IsVaapiSupported(state))
-                    {
-                        return GetAvailableEncoder(mediaEncoder, "h264_vaapi", defaultEncoder);
-                    }
-                }
-            }
-
-            return defaultEncoder;
-        }
-
-        private static bool IsVaapiSupported(EncodingJob state)
-        {
-            var videoStream = state.VideoStream;
-
-            if (videoStream != null)
-            {
-                // vaapi will throw an error with this input
-                // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99.
-                if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase))
-                {
-                    if (videoStream.Level == -99 || videoStream.Level == 15)
-                    {
-                        return false;
-                    }
-                }
-            }
-            return true;
-        }
-
-        internal static bool CanStreamCopyVideo(EncodingJobOptions request, MediaStream videoStream)
-        {
-            if (videoStream.IsInterlaced)
-            {
-                return false;
-            }
-
-            if (videoStream.IsAnamorphic ?? false)
-            {
-                return false;
-            }
-
-            // Can't stream copy if we're burning in subtitles
-            if (request.SubtitleStreamIndex.HasValue)
-            {
-                if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
-                {
-                    return false;
-                }
-            }
-
-            // Source and target codecs must match
-            if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
-            {
-                if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value)
-                {
-                    return false;
-                }
-            }
-
-            // If client is requesting a specific video profile, it must match the source
-            if (!string.IsNullOrEmpty(request.Profile))
-            {
-                if (string.IsNullOrEmpty(videoStream.Profile))
-                {
-                    return false;
-                }
-
-                if (!string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
-                {
-                    var currentScore = GetVideoProfileScore(videoStream.Profile);
-                    var requestedScore = GetVideoProfileScore(request.Profile);
-
-                    if (currentScore == -1 || currentScore > requestedScore)
-                    {
-                        return false;
-                    }
-                }
-            }
-
-            // Video width must fall within requested value
-            if (request.MaxWidth.HasValue)
-            {
-                if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)
-                {
-                    return false;
-                }
-            }
-
-            // Video height must fall within requested value
-            if (request.MaxHeight.HasValue)
-            {
-                if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)
-                {
-                    return false;
-                }
-            }
-
-            // Video framerate must fall within requested value
-            var requestedFramerate = request.MaxFramerate ?? request.Framerate;
-            if (requestedFramerate.HasValue)
-            {
-                var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate;
-
-                if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
-                {
-                    return false;
-                }
-            }
-
-            // Video bitrate must fall within requested value
-            if (request.VideoBitRate.HasValue)
-            {
-                if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)
-                {
-                    return false;
-                }
-            }
-
-            if (request.MaxVideoBitDepth.HasValue)
-            {
-                if (videoStream.BitDepth.HasValue && videoStream.BitDepth.Value > request.MaxVideoBitDepth.Value)
-                {
-                    return false;
-                }
-            }
-
-            if (request.MaxRefFrames.HasValue)
-            {
-                if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > request.MaxRefFrames.Value)
-                {
-                    return false;
-                }
-            }
-
-            // If a specific level was requested, the source must match or be less than
-            if (request.Level.HasValue)
-            {
-                if (!videoStream.Level.HasValue)
-                {
-                    return false;
-                }
-
-                if (videoStream.Level.Value > request.Level.Value)
-                {
-                    return false;
-                }
-            }
-
-            return request.EnableAutoStreamCopy;
-        }
-
         private static int GetVideoProfileScore(string profile)
         {
             var list = new List<string>
@@ -780,57 +237,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase));
         }
 
-        internal static bool CanStreamCopyAudio(EncodingJobOptions request, MediaStream audioStream, List<string> supportedAudioCodecs)
-        {
-            // Source and target codecs must match
-            if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            // Video bitrate must fall within requested value
-            if (request.AudioBitRate.HasValue)
-            {
-                if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0)
-                {
-                    return false;
-                }
-                if (audioStream.BitRate.Value > request.AudioBitRate.Value)
-                {
-                    return false;
-                }
-            }
-
-            // Channels must fall within requested value
-            var channels = request.AudioChannels ?? request.MaxAudioChannels;
-            if (channels.HasValue)
-            {
-                if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0)
-                {
-                    return false;
-                }
-                if (audioStream.Channels.Value > channels.Value)
-                {
-                    return false;
-                }
-            }
-
-            // Sample rate must fall within requested value
-            if (request.AudioSampleRate.HasValue)
-            {
-                if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0)
-                {
-                    return false;
-                }
-                if (audioStream.SampleRate.Value > request.AudioSampleRate.Value)
-                {
-                    return false;
-                }
-            }
-
-            return request.EnableAutoStreamCopy;
-        }
-
         private void ApplyDeviceProfileSettings(EncodingJob state)
         {
             var profile = state.Options.DeviceProfile;

+ 118 - 0
MediaBrowser.MediaEncoding/Encoder/EncodingJobInfo.cs

@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.MediaEncoding.Encoder
+{
+    // For now, a common base class until the API and MediaEncoding classes are unified
+    public class EncodingJobInfo
+    {
+        private readonly ILogger _logger;
+
+        public MediaStream VideoStream { get; set; }
+        public VideoType VideoType { get; set; }
+        public Dictionary<string, string> RemoteHttpHeaders { get; set; }
+        public string OutputVideoCodec { get; set; }
+        public MediaProtocol InputProtocol { get; set; }
+        public string MediaPath { get; set; }
+        public bool IsInputVideo { get; set; }
+        public IIsoMount IsoMount { get; set; }
+        public List<string> PlayableStreamFileNames { get; set; }
+        public string OutputAudioCodec { get; set; }
+        public int? OutputVideoBitrate { get; set; }
+        public MediaStream SubtitleStream { get; set; }
+        public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+
+        public int InternalSubtitleStreamOffset { get; set; }
+        public MediaSourceInfo MediaSource { get; set; }
+        public User User { get; set; }
+
+        public long? RunTimeTicks { get; set; }
+
+        public bool ReadInputAtNativeFramerate { get; set; }
+
+        public string OutputContainer { get; set; }
+
+        public string OutputVideoSync = "-1";
+        public string OutputAudioSync = "1";
+        public string InputAudioSync { get; set; }
+        public string InputVideoSync { get; set; }
+        public TransportStreamTimestamp InputTimestamp { get; set; }
+
+        public MediaStream AudioStream { get; set; }
+        public List<string> SupportedAudioCodecs { get; set; }
+        public List<string> SupportedVideoCodecs { get; set; }
+        public string InputContainer { get; set; }
+        public IsoType? IsoType { get; set; }
+
+        public BaseEncodingJobOptions BaseRequest { get; set; }
+
+        public long? StartTimeTicks
+        {
+            get { return BaseRequest.StartTimeTicks; }
+        }
+
+        public bool CopyTimestamps
+        {
+            get { return BaseRequest.CopyTimestamps; }
+        }
+
+        public int? OutputAudioChannels;
+        public int? OutputAudioSampleRate;
+        public bool DeInterlace { get; set; }
+        public bool IsVideoRequest { get; set; }
+
+        public EncodingJobInfo(ILogger logger)
+        {
+            _logger = logger;
+            RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            PlayableStreamFileNames = new List<string>();
+            SupportedVideoCodecs = new List<string>();
+            SupportedVideoCodecs = new List<string>();
+        }
+
+        /// <summary>
+        /// Predicts the audio sample rate that will be in the output stream
+        /// </summary>
+        public double? TargetVideoLevel
+        {
+            get
+            {
+                var stream = VideoStream;
+                var request = BaseRequest;
+
+                return !string.IsNullOrEmpty(request.Level) && !request.Static
+                    ? double.Parse(request.Level, CultureInfo.InvariantCulture)
+                    : stream == null ? null : stream.Level;
+            }
+        }
+
+        protected void DisposeIsoMount()
+        {
+            if (IsoMount != null)
+            {
+                try
+                {
+                    IsoMount.Dispose();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing iso mount", ex);
+                }
+
+                IsoMount = null;
+            }
+        }
+    }
+}

+ 16 - 10
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -496,6 +496,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return SupportsEncoder(codec);
         }
 
+        public bool CanEncodeToSubtitleCodec(string codec)
+        {
+            // TODO
+            return true;
+        }
+
         /// <summary>
         /// Gets the encoder path.
         /// </summary>
@@ -693,16 +699,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         private async Task<bool> DetectInterlaced(MediaSourceInfo video, MediaStream videoStream, string inputPath, string probeSizeArgument)
         {
-            if (video.Protocol != MediaProtocol.File)
-            {
-                // If it's mpeg based, assume true
-                if ((videoStream.Codec ?? string.Empty).IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    return true;
-                }
-                return false;
-            }
-
             var formats = (video.Container ?? string.Empty).Split(',').ToList();
             var enableInterlacedDection = formats.Contains("vob", StringComparer.OrdinalIgnoreCase) ||
                                           formats.Contains("m2ts", StringComparer.OrdinalIgnoreCase) ||
@@ -727,6 +723,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 }
             }
 
+            if (video.Protocol != MediaProtocol.File)
+            {
+                // If it's mpeg based, assume true
+                if ((videoStream.Codec ?? string.Empty).IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    return true;
+                }
+                return false;
+            }
+
             var args = "{0} -i {1} -map 0:v:{2} -an -filter:v idet -frames:v 500 -an -f null /dev/null";
 
             var process = _processFactory.Create(new ProcessOptions

+ 12 - 11
MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs

@@ -21,7 +21,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         protected override async Task<string> GetCommandLineArguments(EncodingJob state)
         {
             // Get the output codec name
-            var videoCodec = EncodingJobFactory.GetVideoEncoder(MediaEncoder, state, GetEncodingOptions());
+            var encodingOptions = GetEncodingOptions();
+            var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
 
             var format = string.Empty;
             var keyFrame = string.Empty;
@@ -33,17 +34,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 format = " -f mp4 -movflags frag_keyframe+empty_moov";
             }
 
-            var threads = GetNumberOfThreads(state, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
+            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
 
-            var inputModifier = GetInputModifier(state);
+            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
             var videoArguments = await GetVideoArguments(state, videoCodec).ConfigureAwait(false);
 
             return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"",
                 inputModifier,
-                GetInputArgument(state),
+                EncodingHelper.GetInputArgument(state, encodingOptions),
                 keyFrame,
-                GetMapArgs(state),
+                EncodingHelper.GetMapArgs(state),
                 videoArguments,
                 threads,
                 GetAudioArguments(state),
@@ -76,7 +77,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
             {
-                if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
                 {
                     args += " -bsf:v h264_mp4toannexb";
                 }
@@ -94,10 +95,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
             {
-                args += await GetOutputSizeParam(state, videoCodec).ConfigureAwait(false);
+                args += EncodingHelper.GetOutputSizeParam(state, videoCodec);
             }
 
-            var qualityParam = GetVideoQualityParam(state, videoCodec);
+            var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, GetEncodingOptions(), "superfast");
 
             if (!string.IsNullOrEmpty(qualityParam))
             {
@@ -107,7 +108,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             // This is for internal graphical subs
             if (hasGraphicalSubs)
             {
-                args += await GetGraphicalSubtitleParam(state, videoCodec).ConfigureAwait(false);
+                args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec);
             }
 
             return args;
@@ -127,7 +128,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
 
             // Get the output codec name
-            var codec = EncodingJobFactory.GetAudioEncoder(state);
+            var codec = EncodingHelper.GetAudioEncoder(state);
 
             var args = "-codec:a:0 " + codec;
 
@@ -151,7 +152,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
             }
 
-            args += " " + GetAudioFilterParam(state, false);
+            args += " " + EncodingHelper.GetAudioFilterParam(state, GetEncodingOptions(), false);
 
             return args;
         }

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

@@ -50,8 +50,10 @@
     <Compile Include="Configuration\EncodingConfigurationFactory.cs" />
     <Compile Include="Encoder\AudioEncoder.cs" />
     <Compile Include="Encoder\BaseEncoder.cs" />
+    <Compile Include="Encoder\EncodingHelper.cs" />
     <Compile Include="Encoder\EncodingJob.cs" />
     <Compile Include="Encoder\EncodingJobFactory.cs" />
+    <Compile Include="Encoder\EncodingJobInfo.cs" />
     <Compile Include="Encoder\EncodingUtils.cs" />
     <Compile Include="Encoder\EncoderValidator.cs" />
     <Compile Include="Encoder\FontConfigLoader.cs" />

+ 1 - 8
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -90,13 +90,11 @@ namespace MediaBrowser.MediaEncoding.Probing
             }
 
             FetchGenres(info, tags);
-            var shortOverview = FFProbeHelpers.GetDictionaryValue(tags, "description");
             var overview = FFProbeHelpers.GetDictionaryValue(tags, "synopsis");
 
             if (string.IsNullOrWhiteSpace(overview))
             {
-                overview = shortOverview;
-                shortOverview = null;
+                overview = FFProbeHelpers.GetDictionaryValue(tags, "description");
             }
             if (string.IsNullOrWhiteSpace(overview))
             {
@@ -108,11 +106,6 @@ namespace MediaBrowser.MediaEncoding.Probing
                 info.Overview = overview;
             }
 
-            if (!string.IsNullOrWhiteSpace(shortOverview))
-            {
-                info.ShortOverview = shortOverview;
-            }
-
             var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
             if (!string.IsNullOrWhiteSpace(title))
             {

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

@@ -1,8 +0,0 @@
-namespace MediaBrowser.Model.Configuration
-{
-    public class PathSubstitution
-    {
-        public string From { get; set; }
-        public string To { get; set; }
-    }
-}

+ 2 - 6
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -47,6 +47,7 @@ namespace MediaBrowser.Model.Configuration
         /// <value><c>true</c> if [use HTTPS]; otherwise, <c>false</c>.</value>
         public bool EnableHttps { get; set; }
         public bool EnableSeriesPresentationUniqueKey { get; set; }
+        public bool EnableLocalizedGuids { get; set; }
 
         /// <summary>
         /// Gets or sets the value pointing to the file system where the ssl certiifcate is located..
@@ -163,8 +164,6 @@ namespace MediaBrowser.Model.Configuration
         public bool SkipDeserializationForPrograms { get; set; }
         public bool SkipDeserializationForAudio { get; set; }
 
-        public PathSubstitution[] PathSubstitutions { get; set; }
-
         public string ServerName { get; set; }
         public string WanDdns { get; set; }
 
@@ -182,7 +181,6 @@ namespace MediaBrowser.Model.Configuration
 
         public bool EnableAnonymousUsageReporting { get; set; }
         public bool EnableStandaloneMusicKeys { get; set; }
-        public bool EnableLocalizedGuids { get; set; }
         public bool EnableFolderView { get; set; }
         public bool EnableGroupingIntoCollections { get; set; }
         public bool DisplaySpecialsWithinSeasons { get; set; }
@@ -192,7 +190,6 @@ namespace MediaBrowser.Model.Configuration
         public string[] Migrations { get; set; }
         public bool EnableChannelView { get; set; }
         public bool EnableExternalContentInSuggestions { get; set; }
-        public bool EnableSimpleArtistDetection { get; set; }
 
         public int ImageExtractionTimeoutMs { get; set; }
         /// <summary>
@@ -204,8 +201,8 @@ namespace MediaBrowser.Model.Configuration
             CodecsUsed = new string[] { };
             Migrations = new string[] { };
             ImageExtractionTimeoutMs = 0;
-
             EnableLocalizedGuids = true;
+
             DisplaySpecialsWithinSeasons = true;
             EnableExternalContentInSuggestions = true;
 
@@ -231,7 +228,6 @@ namespace MediaBrowser.Model.Configuration
 
             LibraryMonitorDelay = 60;
 
-            PathSubstitutions = new PathSubstitution[] { };
             ContentTypes = new NameValuePair[] { };
 
             PreferredMetadataLanguage = "en";

+ 5 - 0
MediaBrowser.Model/Dlna/ITranscoderSupport.cs

@@ -3,6 +3,7 @@
     public interface ITranscoderSupport
     {
         bool CanEncodeToAudioCodec(string codec);
+        bool CanEncodeToSubtitleCodec(string codec);
     }
 
     public class FullTranscoderSupport : ITranscoderSupport
@@ -11,5 +12,9 @@
         {
             return true;
         }
+        public bool CanEncodeToSubtitleCodec(string codec)
+        {
+            return true;
+        }
     }
 }

+ 58 - 6
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -435,7 +435,7 @@ namespace MediaBrowser.Model.Dlna
 
                     if (subtitleStream != null)
                     {
-                        SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value);
+                        SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, null, null);
 
                         playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
                         playlistItem.SubtitleFormat = subtitleProfile.Format;
@@ -465,10 +465,11 @@ namespace MediaBrowser.Model.Dlna
 
                 if (subtitleStream != null)
                 {
-                    SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode);
+                    SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, transcodingProfile.Protocol, transcodingProfile.Container);
 
                     playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
                     playlistItem.SubtitleFormat = subtitleProfile.Format;
+                    playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
                 }
 
                 playlistItem.PlayMethod = PlayMethod.Transcode;
@@ -874,7 +875,7 @@ namespace MediaBrowser.Model.Dlna
         {
             if (subtitleStream != null)
             {
-                SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, playMethod);
+                SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, playMethod, null, null);
 
                 if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
                 {
@@ -886,11 +887,11 @@ namespace MediaBrowser.Model.Dlna
             return IsAudioEligibleForDirectPlay(item, maxBitrate);
         }
 
-        public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod)
+        public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, string transcodingSubProtocol, string transcodingContainer)
         {
-            if (playMethod != PlayMethod.Transcode && !subtitleStream.IsExternal)
+            if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
             {
-                // Look for supported embedded subs
+                // Look for supported embedded subs of the same format
                 foreach (SubtitleProfile profile in subtitleProfiles)
                 {
                     if (!profile.SupportsLanguage(subtitleStream.Language))
@@ -903,11 +904,40 @@ namespace MediaBrowser.Model.Dlna
                         continue;
                     }
 
+                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, transcodingContainer))
+                    {
+                        continue;
+                    }
+
                     if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && StringHelper.EqualsIgnoreCase(profile.Format, subtitleStream.Codec))
                     {
                         return profile;
                     }
                 }
+
+                // Look for supported embedded subs of a convertible format
+                foreach (SubtitleProfile profile in subtitleProfiles)
+                {
+                    if (!profile.SupportsLanguage(subtitleStream.Language))
+                    {
+                        continue;
+                    }
+
+                    if (profile.Method != SubtitleDeliveryMethod.Embed)
+                    {
+                        continue;
+                    }
+
+                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, transcodingContainer))
+                    {
+                        continue;
+                    }
+
+                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format))
+                    {
+                        return profile;
+                    }
+                }
             }
 
             // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require conversion
@@ -918,6 +948,28 @@ namespace MediaBrowser.Model.Dlna
             };
         }
 
+        private static bool IsSubtitleEmbedSupported(MediaStream subtitleStream, SubtitleProfile subtitleProfile, string transcodingSubProtocol, string transcodingContainer)
+        {
+            if (string.Equals(transcodingContainer, "ts", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+            if (string.Equals(transcodingContainer, "mpegts", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+            if (string.Equals(transcodingContainer, "mp4", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+            if (string.Equals(transcodingContainer, "mkv", StringComparison.OrdinalIgnoreCase))
+            {
+                return true;
+            }
+
+            return false;
+        }
+
         private static SubtitleProfile GetExternalSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, bool allowConversion)
         {
             foreach (SubtitleProfile profile in subtitleProfiles)

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

@@ -18,6 +18,7 @@ namespace MediaBrowser.Model.Dlna
         public StreamInfo()
         {
             AudioCodecs = new string[] { };
+            SubtitleCodecs = new string[] { };
         }
 
         public string ItemId { get; set; }
@@ -74,6 +75,7 @@ namespace MediaBrowser.Model.Dlna
 
         public MediaSourceInfo MediaSource { get; set; }
 
+        public string[] SubtitleCodecs { get; set; }
         public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
         public string SubtitleFormat { get; set; }
 
@@ -268,6 +270,12 @@ namespace MediaBrowser.Model.Dlna
             list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
             list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString().ToLower()));
 
+            string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
+               string.Empty :
+               string.Join(",", item.SubtitleCodecs);
+
+            list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
+
             return list;
         }
 
@@ -354,7 +362,7 @@ namespace MediaBrowser.Model.Dlna
 
         private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles)
         {
-            SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, subtitleProfiles, PlayMethod);
+            SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, subtitleProfiles, PlayMethod, SubProtocol, Container);
             SubtitleStreamInfo info = new SubtitleStreamInfo
             {
                 IsForced = stream.IsForced,

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

@@ -211,12 +211,6 @@ namespace MediaBrowser.Model.Dto
         /// <value>The overview.</value>
         public string Overview { get; set; }
 
-        /// <summary>
-        /// Gets or sets the short overview.
-        /// </summary>
-        /// <value>The short overview.</value>
-        public string ShortOverview { get; set; }
-
         /// <summary>
         /// Gets or sets the taglines.
         /// </summary>

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

@@ -311,29 +311,31 @@ namespace MediaBrowser.Model.Entities
                    !StringHelper.EqualsIgnoreCase(codec, "dvb_subtitle");
         }
 
-        public bool SupportsSubtitleConversionTo(string codec)
+        public bool SupportsSubtitleConversionTo(string toCodec)
         {
             if (!IsTextSubtitleStream)
             {
                 return false;
             }
 
+            var fromCodec = Codec;
+
             // Can't convert from this 
-            if (StringHelper.EqualsIgnoreCase(Codec, "ass"))
+            if (StringHelper.EqualsIgnoreCase(fromCodec, "ass"))
             {
                 return false;
             }
-            if (StringHelper.EqualsIgnoreCase(Codec, "ssa"))
+            if (StringHelper.EqualsIgnoreCase(fromCodec, "ssa"))
             {
                 return false;
             }
 
             // Can't convert to this 
-            if (StringHelper.EqualsIgnoreCase(codec, "ass"))
+            if (StringHelper.EqualsIgnoreCase(toCodec, "ass"))
             {
                 return false;
             }
-            if (StringHelper.EqualsIgnoreCase(codec, "ssa"))
+            if (StringHelper.EqualsIgnoreCase(toCodec, "ssa"))
             {
                 return false;
             }

+ 0 - 12
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -96,17 +96,5 @@ namespace MediaBrowser.Model.LiveTv
             EnableAllTuners = true;
             ChannelMappings = new NameValuePair[] {};
         }
-
-        public string GetMappedChannel(string channelNumber)
-        {
-            foreach (NameValuePair mapping in ChannelMappings)
-            {
-                if (StringHelper.EqualsIgnoreCase(mapping.Name, channelNumber))
-                {
-                    return mapping.Value;
-                }
-            }
-            return channelNumber;
-        }
     }
 }

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

@@ -198,7 +198,6 @@
     <Compile Include="Notifications\NotificationOption.cs" />
     <Compile Include="Notifications\NotificationOptions.cs" />
     <Compile Include="Notifications\NotificationType.cs" />
-    <Compile Include="Configuration\PathSubstitution.cs" />
     <Compile Include="Notifications\SendToUserType.cs" />
     <Compile Include="Configuration\ServerConfiguration.cs" />
     <Compile Include="Playlists\PlaylistCreationRequest.cs" />

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

@@ -51,11 +51,6 @@ namespace MediaBrowser.Model.MediaInfo
         /// </summary>
         /// <value>The overview.</value>
         public string Overview { get; set; }
-        /// <summary>
-        /// Gets or sets the short overview.
-        /// </summary>
-        /// <value>The short overview.</value>
-        public string ShortOverview { get; set; }
 
         public MediaInfo()
         {

+ 2 - 1
MediaBrowser.Model/Net/IUdpSocket.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Model.Net
@@ -22,6 +23,6 @@ namespace MediaBrowser.Model.Net
         /// <summary>
         /// Sends a UDP message to a particular end point (uni or multicast).
         /// </summary>
-        Task SendAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint);
+        Task SendAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken);
     }
 }

+ 0 - 5
MediaBrowser.Model/Querying/ItemFields.cs

@@ -188,11 +188,6 @@
         /// </summary>
         Settings,
 
-        /// <summary>
-        /// The short overview
-        /// </summary>
-        ShortOverview,
-
         /// <summary>
         /// The screenshot image tags
         /// </summary>

+ 2 - 0
MediaBrowser.Model/Session/PlaybackStopInfo.cs

@@ -47,5 +47,7 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// <value><c>true</c> if failed; otherwise, <c>false</c>.</value>
         public bool Failed { get; set; }
+
+        public string NextMediaType { get; set; }
     }
 }

+ 7 - 3
MediaBrowser.Providers/Manager/ImageSaver.cs

@@ -16,7 +16,6 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Controller.IO;
 using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.Providers.Manager
@@ -234,6 +233,7 @@ namespace MediaBrowser.Providers.Manager
             return retryPath;
         }
 
+        private SemaphoreSlim _imageSaveSemaphore = new SemaphoreSlim(1, 1);
         /// <summary>
         /// Saves the image to location.
         /// </summary>
@@ -247,11 +247,13 @@ namespace MediaBrowser.Providers.Manager
 
             var parentFolder = Path.GetDirectoryName(path);
 
-            _libraryMonitor.ReportFileSystemChangeBeginning(path);
-            _libraryMonitor.ReportFileSystemChangeBeginning(parentFolder);
+            await _imageSaveSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 
             try
             {
+                _libraryMonitor.ReportFileSystemChangeBeginning(path);
+                _libraryMonitor.ReportFileSystemChangeBeginning(parentFolder);
+
                 _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
 
                 // If the file is currently hidden we'll have to remove that or the save will fail
@@ -283,6 +285,8 @@ namespace MediaBrowser.Providers.Manager
             }
             finally
             {
+                _imageSaveSemaphore.Release();
+
                 _libraryMonitor.ReportFileSystemChangeComplete(path, false);
                 _libraryMonitor.ReportFileSystemChangeComplete(parentFolder, false);
             }

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

@@ -253,7 +253,7 @@ namespace MediaBrowser.Providers.Manager
             {
                 try
                 {
-                    await ProviderManager.SaveImage(personEntity, imageUrl, null, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
+                    await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
                     return;
                 }
                 catch (Exception ex)

+ 1 - 2
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -123,12 +123,11 @@ namespace MediaBrowser.Providers.Manager
             return Task.FromResult(ItemUpdateType.None);
         }
 
-        public async Task SaveImage(IHasImages item, string url, SemaphoreSlim resourcePool, ImageType type, int? imageIndex, CancellationToken cancellationToken)
+        public async Task SaveImage(IHasImages item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         {
             var response = await _httpClient.GetResponse(new HttpRequestOptions
             {
                 CancellationToken = cancellationToken,
-                ResourcePool = resourcePool,
                 Url = url,
                 BufferContent = false
 

+ 0 - 9
MediaBrowser.Providers/Manager/ProviderUtils.cs

@@ -200,7 +200,6 @@ namespace MediaBrowser.Providers.Manager
             MergeCriticRating(source, target, lockedFields, replaceData);
             MergeAwards(source, target, lockedFields, replaceData);
             MergeTrailers(source, target, lockedFields, replaceData);
-            MergeShortOverview(source, target, lockedFields, replaceData);
 
             if (mergeMetadataSettings)
             {
@@ -234,14 +233,6 @@ namespace MediaBrowser.Providers.Manager
             }
         }
 
-        private static void MergeShortOverview(BaseItem source, BaseItem target, List<MetadataFields> lockedFields, bool replaceData)
-        {
-            if (replaceData || string.IsNullOrEmpty(target.ShortOverview))
-            {
-                target.ShortOverview = source.ShortOverview;
-            }
-        }
-
         private static void MergeAlbumArtist(BaseItem source, BaseItem target, List<MetadataFields> lockedFields, bool replaceData)
         {
             var sourceHasAlbumArtist = source as IHasAlbumArtist;

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

@@ -440,11 +440,6 @@ namespace MediaBrowser.Providers.MediaInfo
                     video.Overview = data.Overview;
                 }
             }
-
-            if (string.IsNullOrWhiteSpace(video.ShortOverview) || isFullRefresh)
-            {
-                video.ShortOverview = data.ShortOverview;
-            }
         }
 
         private async Task FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options)

+ 1 - 1
MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs

@@ -124,7 +124,7 @@ namespace MediaBrowser.Providers.MediaInfo
         {
             get
             {
-                return new[] { ".srt", ".ssa", ".ass", ".sub", ".smi", ".sami", ".txt" };
+                return new[] { ".srt", ".ssa", ".ass", ".sub", ".smi", ".sami" };
             }
         }
 

+ 6 - 1
MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs

@@ -271,7 +271,12 @@ namespace MediaBrowser.Providers.Movies
             //and the rest from crew
             if (movieData.casts != null && movieData.casts.crew != null)
             {
-                var keepTypes = new[] { PersonType.Director, PersonType.Writer, PersonType.Producer };
+                var keepTypes = new[]
+                {
+                    PersonType.Director,
+                    PersonType.Writer,
+                    //PersonType.Producer
+                };
 
                 foreach (var person in movieData.casts.crew)
                 {

+ 1 - 8
MediaBrowser.Providers/Omdb/OmdbItemProvider.cs

@@ -127,14 +127,7 @@ namespace MediaBrowser.Providers.Omdb
                 }
             }
 
-            using (var stream = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = url,
-                ResourcePool = OmdbProvider.ResourcePool,
-                CancellationToken = cancellationToken,
-                BufferContent = true
-
-            }).ConfigureAwait(false))
+            using (var stream = await OmdbProvider.GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
             {
                 var resultList = new List<SearchResult>();
 

+ 15 - 18
MediaBrowser.Providers/Omdb/OmdbProvider.cs

@@ -294,16 +294,9 @@ namespace MediaBrowser.Providers.Omdb
                 }
             }
 
-            var url = string.Format("https://www.omdbapi.com/?i={0}&tomatoes=true", imdbParam);
+            var url = string.Format("https://www.omdbapi.com/?i={0}&plot=full&tomatoes=true&r=json", imdbParam);
 
-            using (var stream = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = url,
-                ResourcePool = ResourcePool,
-                CancellationToken = cancellationToken,
-                BufferContent = true
-
-            }).ConfigureAwait(false))
+            using (var stream = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
             {
                 var rootObject = _jsonSerializer.DeserializeFromStream<RootObject>(stream);
                 _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
@@ -337,14 +330,7 @@ namespace MediaBrowser.Providers.Omdb
 
             var url = string.Format("https://www.omdbapi.com/?i={0}&season={1}&detail=full", imdbParam, seasonId);
 
-            using (var stream = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = url,
-                ResourcePool = ResourcePool,
-                CancellationToken = cancellationToken,
-                BufferContent = true
-
-            }).ConfigureAwait(false))
+            using (var stream = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
             {
                 var rootObject = _jsonSerializer.DeserializeFromStream<SeasonRootObject>(stream);
                 _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
@@ -354,6 +340,17 @@ namespace MediaBrowser.Providers.Omdb
             return path;
         }
 
+        public static Task<Stream> GetOmdbResponse(IHttpClient httpClient, string url, CancellationToken cancellationToken)
+        {
+            return httpClient.Get(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = ResourcePool,
+                CancellationToken = cancellationToken,
+                BufferContent = true
+            });
+        }
+
         internal string GetDataFilePath(string imdbId)
         {
             if (string.IsNullOrEmpty(imdbId))
@@ -421,7 +418,7 @@ namespace MediaBrowser.Providers.Omdb
             }
 
             // Imdb plots are usually pretty short
-            item.ShortOverview = result.Plot;
+            item.Overview = result.Plot;
 
             //if (!string.IsNullOrWhiteSpace(result.Director))
             //{

+ 6 - 1
MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs

@@ -167,7 +167,12 @@ namespace MediaBrowser.Providers.TV
                     //and the rest from crew
                     if (credits.crew != null)
                     {
-                        var keepTypes = new[] { PersonType.Director, PersonType.Writer, PersonType.Producer };
+                        var keepTypes = new[]
+                        {
+                            PersonType.Director,
+                            //PersonType.Writer,
+                            //PersonType.Producer
+                        };
 
                         foreach (var person in credits.crew)
                         {

+ 16 - 122
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -76,6 +76,11 @@ namespace MediaBrowser.WebDashboard.Api
         public string V { get; set; }
     }
 
+    [Route("/favicon.ico", "GET")]
+    public class GetFavIcon
+    {
+    }
+
     /// <summary>
     /// Class DashboardService
     /// </summary>
@@ -134,6 +139,14 @@ namespace MediaBrowser.WebDashboard.Api
             _memoryStreamFactory = memoryStreamFactory;
         }
 
+        public object Get(GetFavIcon request)
+        {
+            return Get(new GetDashboardResource
+            {
+                ResourceName = "favicon.ico"
+            });
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -323,48 +336,11 @@ namespace MediaBrowser.WebDashboard.Api
             return new PackageCreator(_fileSystem, _logger, _serverConfigurationManager, _memoryStreamFactory);
         }
 
-        private List<string> GetDeployIgnoreExtensions()
-        {
-            var list = new List<string>();
-
-            list.Add(".log");
-            list.Add(".txt");
-            list.Add(".map");
-            list.Add(".md");
-            list.Add(".gz");
-            list.Add(".bat");
-            list.Add(".sh");
-
-            return list;
-        }
-
-        private List<Tuple<string, bool>> GetDeployIgnoreFilenames()
-        {
-            var list = new List<Tuple<string, bool>>();
-
-            list.Add(new Tuple<string, bool>("copying", true));
-            list.Add(new Tuple<string, bool>("license", true));
-            list.Add(new Tuple<string, bool>("license-mit", true));
-            list.Add(new Tuple<string, bool>("gitignore", false));
-            list.Add(new Tuple<string, bool>("npmignore", false));
-            list.Add(new Tuple<string, bool>("jshintrc", false));
-            list.Add(new Tuple<string, bool>("gruntfile", false));
-            list.Add(new Tuple<string, bool>("bowerrc", false));
-            list.Add(new Tuple<string, bool>("jscsrc", false));
-            list.Add(new Tuple<string, bool>("hero.svg", false));
-            list.Add(new Tuple<string, bool>("travis.yml", false));
-            list.Add(new Tuple<string, bool>("build.js", false));
-            list.Add(new Tuple<string, bool>("editorconfig", false));
-            list.Add(new Tuple<string, bool>("gitattributes", false));
-
-            return list;
-        }
-
         public async Task<object> Get(GetDashboardPackage request)
         {
             var mode = request.Mode;
 
-            var path = string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase) ?
+            var path = !string.IsNullOrWhiteSpace(mode) ?
                 Path.Combine(_serverConfigurationManager.ApplicationPaths.ProgramDataPath, "webclient-dump")
                 : "C:\\dev\\emby-web-mobile\\src";
 
@@ -388,101 +364,19 @@ namespace MediaBrowser.WebDashboard.Api
             // Try to trim the output size a bit
             var bowerPath = Path.Combine(path, "bower_components");
 
-            foreach (var ext in GetDeployIgnoreExtensions())
-            {
-                DeleteFilesByExtension(bowerPath, ext);
-            }
-
-            DeleteFilesByExtension(bowerPath, ".json", "strings\\");
-
-            foreach (var ignore in GetDeployIgnoreFilenames())
+            if (!string.IsNullOrWhiteSpace(mode))
             {
-                DeleteFilesByName(bowerPath, ignore.Item1, ignore.Item2);
-            }
-
-            DeleteFoldersByName(bowerPath, "demo");
-            DeleteFoldersByName(bowerPath, "test");
-            DeleteFoldersByName(bowerPath, "guides");
-            DeleteFoldersByName(bowerPath, "grunt");
-            DeleteFoldersByName(bowerPath, "rollups");
+                // Delete things that are unneeded in an attempt to keep the output as trim as possible
 
-            if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
-            {
-                DeleteFoldersByName(Path.Combine(bowerPath, "emby-webcomponents", "fonts"), "montserrat");
-                DeleteFoldersByName(Path.Combine(bowerPath, "emby-webcomponents", "fonts"), "opensans");
                 DeleteFoldersByName(Path.Combine(bowerPath, "emby-webcomponents", "fonts"), "roboto");
-            }
-
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "jquery", "src"), true);
-
-            DeleteCryptoFiles(Path.Combine(bowerPath, "cryptojslib", "components"));
-
-            DeleteFoldersByName(Path.Combine(bowerPath, "jquery"), "src");
-            DeleteFoldersByName(Path.Combine(bowerPath, "jstree"), "src");
-            //DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "meteor");
-            //DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "st");
-            //DeleteFoldersByName(Path.Combine(bowerPath, "Swiper"), "src");
-
-            if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
-            {
-                // Delete things that are unneeded in an attempt to keep the output as trim as possible
                 _fileSystem.DeleteDirectory(Path.Combine(path, "css", "images", "tour"), true);
             }
 
             await DumpHtml(creator.DashboardUIPath, path, mode, culture, appVersion);
 
-            await DumpFile("css/all.css", Path.Combine(path, "css", "all.css"), mode, culture, appVersion).ConfigureAwait(false);
-
             return "";
         }
 
-        private void DeleteCryptoFiles(string path)
-        {
-            var files = _fileSystem.GetFiles(path)
-                .ToList();
-
-            var keepFiles = new[] { "core-min.js", "md5-min.js", "sha1-min.js" };
-
-            foreach (var file in files)
-            {
-                if (!keepFiles.Contains(file.Name, StringComparer.OrdinalIgnoreCase))
-                {
-                    _fileSystem.DeleteFile(file.FullName);
-                }
-            }
-        }
-
-        private void DeleteFilesByExtension(string path, string extension, string exclude = null)
-        {
-            var files = _fileSystem.GetFiles(path, true)
-                .Where(i => string.Equals(i.Extension, extension, StringComparison.OrdinalIgnoreCase))
-                .ToList();
-
-            foreach (var file in files)
-            {
-                if (!string.IsNullOrWhiteSpace(exclude))
-                {
-                    if (file.FullName.IndexOf(exclude, StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        continue;
-                    }
-                }
-                _fileSystem.DeleteFile(file.FullName);
-            }
-        }
-
-        private void DeleteFilesByName(string path, string name, bool exact = false)
-        {
-            var files = _fileSystem.GetFiles(path, true)
-                .Where(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase) || (!exact && i.Name.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1))
-                .ToList();
-
-            foreach (var file in files)
-            {
-                _fileSystem.DeleteFile(file.FullName);
-            }
-        }
-
         private void DeleteFoldersByName(string path, string name)
         {
             var directories = _fileSystem.GetDirectories(path, true)

+ 15 - 59
MediaBrowser.WebDashboard/Api/PackageCreator.cs

@@ -33,16 +33,7 @@ namespace MediaBrowser.WebDashboard.Api
             string localizationCulture,
             string appVersion)
         {
-            Stream resourceStream;
-
-            if (path.Equals("css/all.css", StringComparison.OrdinalIgnoreCase))
-            {
-                resourceStream = await GetAllCss().ConfigureAwait(false);
-            }
-            else
-            {
-                resourceStream = GetRawResourceStream(path);
-            }
+            var resourceStream = GetRawResourceStream(path);
 
             if (resourceStream != null)
             {
@@ -151,7 +142,7 @@ namespace MediaBrowser.WebDashboard.Api
 
                     html = Encoding.UTF8.GetString(originalBytes, 0, originalBytes.Length);
 
-                    if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
+                    if (!string.IsNullOrWhiteSpace(mode))
                     {
                     }
                     else if (!string.IsNullOrWhiteSpace(path) && !string.Equals(path, "index.html", StringComparison.OrdinalIgnoreCase))
@@ -160,10 +151,13 @@ namespace MediaBrowser.WebDashboard.Api
                         if (index != -1)
                         {
                             html = html.Substring(index);
+
+                            html = html.Substring(html.IndexOf('>') + 1);
+
                             index = html.IndexOf("</body>", StringComparison.OrdinalIgnoreCase);
                             if (index != -1)
                             {
-                                html = html.Substring(0, index+7);
+                                html = html.Substring(0, index);
                             }
                         }
                         var mainFile = _fileSystem.ReadAllText(GetDashboardResourcePath("index.html"));
@@ -214,7 +208,8 @@ namespace MediaBrowser.WebDashboard.Api
         {
             var sb = new StringBuilder();
 
-            if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(mode, "android", StringComparison.OrdinalIgnoreCase))
             {
                 sb.Append("<meta http-equiv=\"Content-Security-Policy\" content=\"default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: gap: file: filesystem: ws: wss:;\">");
             }
@@ -263,11 +258,14 @@ namespace MediaBrowser.WebDashboard.Api
         /// <returns>System.String.</returns>
         private string GetCommonCss(string mode, string version)
         {
-            var versionString = !string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase) ? "?v=" + version : string.Empty;
+            var versionString = string.IsNullOrWhiteSpace(mode) ? "?v=" + version : string.Empty;
 
             var files = new[]
                             {
-                                "css/all.css" + versionString
+                                      "css/site.css" + versionString,
+                                      "css/librarymenu.css" + versionString,
+                                      "css/librarybrowser.css" + versionString,
+                                      "thirdparty/paper-button-style.css" + versionString
                             };
 
             var tags = files.Select(s => string.Format("<link rel=\"stylesheet\" href=\"{0}\" async />", s)).ToArray();
@@ -291,14 +289,14 @@ namespace MediaBrowser.WebDashboard.Api
                 builder.AppendFormat("window.appMode='{0}';", mode);
             }
 
-            if (!string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
+            if (string.IsNullOrWhiteSpace(mode))
             {
                 builder.AppendFormat("window.dashboardVersion='{0}';", version);
             }
 
             builder.Append("</script>");
 
-            var versionString = !string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase) ? "?v=" + version : string.Empty;
+            var versionString = string.IsNullOrWhiteSpace(mode) ? "?v=" + version : string.Empty;
 
             var files = new List<string>();
 
@@ -318,48 +316,6 @@ namespace MediaBrowser.WebDashboard.Api
             return builder.ToString();
         }
 
-        /// <summary>
-        /// Gets all CSS.
-        /// </summary>
-        /// <returns>Task{Stream}.</returns>
-        private async Task<Stream> GetAllCss()
-        {
-            var memoryStream = _memoryStreamFactory.CreateNew();
-
-            var files = new[]
-                                  {
-                                      "css/site.css",
-                                      "css/librarymenu.css",
-                                      "css/librarybrowser.css",
-                                      "thirdparty/paper-button-style.css"
-                                  };
-
-            var builder = new StringBuilder();
-
-            foreach (var file in files)
-            {
-                var path = GetDashboardResourcePath(file);
-
-                using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true))
-                {
-                    using (var streamReader = new StreamReader(fs))
-                    {
-                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
-                        builder.Append(text);
-                        builder.Append(Environment.NewLine);
-                    }
-                }
-            }
-
-            var css = builder.ToString();
-
-            var bytes = Encoding.UTF8.GetBytes(css);
-            memoryStream.Write(bytes, 0, bytes.Length);
-
-            memoryStream.Position = 0;
-            return memoryStream;
-        }
-
         /// <summary>
         /// Gets the raw resource stream.
         /// </summary>

+ 1 - 1429
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -70,1439 +70,11 @@
     </ProjectReference>
   </ItemGroup>
   <ItemGroup>
-    <None Include="dashboard-ui\bower_components\**\*.*">
+    <None Include="dashboard-ui\**\*.*">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
   </ItemGroup>
   <ItemGroup>
-    <Content Include="dashboard-ui\autoorganizesmart.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\camerauploadsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\accessschedule\accessschedule.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\accessschedule\accessschedule.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\appfooter\appfooter.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\appfooter\appfooter.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\apphost.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\categorysyncbuttons.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\channelmapper\channelmapper.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\chromecasthelpers.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\directorybrowser\directorybrowser.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\dockedtabs\dockedtabs.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\dockedtabs\dockedtabs.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\favoriteitems.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\filterdialog\filterdialog.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\filterdialog\filterdialog.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\filterdialog\style.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\groupedcards.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\guestinviter\connectlink.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\guestinviter\connectlink.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\guestinviter\guestinviter.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\guestinviter\guestinviter.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\iap.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\libraryoptionseditor\libraryoptionseditor.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\libraryoptionseditor\libraryoptionseditor.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\navdrawer\navdrawer.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\navdrawer\navdrawer.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\remotecontrol.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\tvproviders\xmltv.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\tvproviders\xmltv.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\throbber.gif">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\camerauploadsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvschedule.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvseriestimers.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\userpasswordpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\viewcontainer-lite.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\dashboard.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\autoorganizetable.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\logo.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\home.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\legacy\fnchecked.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\legacy\buttonenabled.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\directorybrowser\directorybrowser.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\fileorganizer\fileorganizer.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\fileorganizer\fileorganizer.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\humanedate.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\imagedownloader\imagedownloader.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\imagedownloader\imagedownloader.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\imageuploader\imageuploader.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\imageuploader\imageuploader.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\medialibrarycreator\medialibrarycreator.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\medialibrarycreator\medialibrarycreator.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\medialibraryeditor\medialibraryeditor.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\medialibraryeditor\medialibraryeditor.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\ani_equalizer_black.gif">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\ani_equalizer_white.gif">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\androidtv-tile.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\chromecast.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\empty.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\videoosd.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\touchicon144.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\help.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\sync.png">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourmysync.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\nowplayingbar.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\favorites.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\legacy\dashboard.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\legacy\selectmenu.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\librarydisplay.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvguideprovider.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvtunerprovider-hdhomerun.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvtunerprovider-m3u.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvtunerprovider-satip.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mypreferenceshome.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mypreferencesmenu.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mysyncsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\robots.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\autobackdrops.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\homefavorites.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\homenextup.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\homeupcoming.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\librarydisplay.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvguideprovider.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvtunerprovider-hdhomerun.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvtunerprovider-m3u.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\livetvtunerprovider-satip.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\localsync.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\musicfolders.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\mypreferenceshome.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\mysync.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\mysyncsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\autoorganizesmart.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\searchpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\secondaryitems.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\serversecurity.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\shared.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\supporterkeypage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\tvlatest.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\wizardcomponents.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\wizardcontroller.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\wizardlivetvguide.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\wizardlivetvtuner.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\search.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\secondaryitems.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\serversecurity.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\serviceworker.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\shared.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\devices\android\android.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\themes\holiday\style.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\themes\holiday\theme.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\themes\halloween\bg.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\themes\halloween\style.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\themes\halloween\theme.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\devices\ios\ios.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\sections.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.panel.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.panel.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.popup.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.slider.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.table.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.slider.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.widget.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\paper-button-style.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\tvproviders\schedulesdirect.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\components\tvproviders\schedulesdirect.template.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboardhosting.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\forgotpasswordpin.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvitems.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mysync.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mysyncjob.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\photos.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\dashboardhosting.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\forgotpassword.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\forgotpasswordpin.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvcomponents.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvitems.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\mypreferencescommon.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\photos.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\reports.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\selectserver.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\streamingsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\appservices.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\syncsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\taskbutton.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\usernew.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\userpassword.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\wizardagreement.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\selectserver.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\streamingsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\appservices.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\syncsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\channelitems.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\channels.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\cinemamodeconfiguration.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\connectlogin.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\chromecast.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\amazon.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\playstore.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\kodi.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\favicon.ico">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\detail\tv.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\media\pause.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\media\play.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\chapters.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\cinemamode.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\dashboard.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\mobile.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\notifications.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\plugins.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\scheduledtasks.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\subtitles.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\admin\users.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\enjoy.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourcollections.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourcontent.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\toureditor.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourmobile1.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourmobile2.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourmouseover.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourmovies.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourplaylist.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourtaphold.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourusersettings1.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourusersettings2.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourusersettings3.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\tour\web\tourusersettings4.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\librarymenu.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\livetv.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\nowplaying.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboardgeneral.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\syncactivity.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\device.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\devices.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\devicesupload.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dlnaprofile.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dlnaprofiles.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dlnasettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\encodingsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\autoorganizetv.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\autoorganizelog.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\files\dummy.mp4">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\metadatanfo.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mypreferencesdisplay.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\mypreferenceslanguages.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\notificationlist.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\playbackconfiguration.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\playlists.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\reports.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetvstatus.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\metadatasubtitles.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\editor\missing.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\editor\missingbackdrop.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\editor\missinglogo.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\editor\missingprimaryimage.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\metadataeditor.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\notifications.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\detailtable.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\chrome.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\mbc.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\roku.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\fresh.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\media\chapterflyout.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\rotten.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\detail\person.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\list\chapter.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\list\person.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\supporter\nonsupporterbadge.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\userdata\administrator.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\userdata\password.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\librarybrowser.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\edititemmetadata.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\gamegenres.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\games.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\gamesrecommended.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\gamestudios.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\gamesystems.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\notificationsetting.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\notificationsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\nowplaying.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\channelitems.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\channels.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\channelslatest.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\cinemamodeconfiguration.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\connectlogin.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\dashboardgeneral.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\syncactivity.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\device.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\devices.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\devicesupload.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\dlnaprofile.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\dlnaprofiles.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\dlnasettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\encodingsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\autoorganizetv.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\autoorganizelog.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\favorites.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\librarymenu.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\metadatanfo.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\mypreferencesdisplay.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\mypreferenceslanguages.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\notificationlist.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\playbackconfiguration.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\playlistedit.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\playlists.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvchannel.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvstatus.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\editorsidebar.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\metadatasubtitles.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvchannels.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvguide.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvrecordings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\notificationsetting.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\notificationsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\nowplayingbar.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\nowplayingpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\episodes.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\livetvsuggested.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\tvupcoming.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\userlibraryaccess.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\userparentalcontrol.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\wizardsettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\livetv.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jstree\themes\default\32px.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jstree\themes\default\40px.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jstree\themes\default\style.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jstree\themes\default\style.min.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jstree\themes\default\throbber.gif">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\userlibraryaccess.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\usernew.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\userparentalcontrol.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\userpassword.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\videoosd.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\wizardagreement.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\wizardcomponents.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\wizardlivetvguide.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\wizardlivetvtuner.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\index.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\forgotpassword.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\dashboard.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\librarysettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\movies.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\music.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\edititemmetadata.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\librarysettings.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\musicrecommended.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\notifications.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\moviecollections.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\gamegenrepage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\gamespage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\gamesrecommendedpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\gamestudiospage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\gamesystemspage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\itembynamedetailpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\moviegenres.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\moviestudios.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\movietrailers.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\musicalbums.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\musicartists.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\musicgenres.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\songs.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\librarybrowser.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\movies.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\moviesrecommended.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\site.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\site.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\library.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\tvgenres.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\tvrecommended.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\tvshows.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\tvstudios.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\tv.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\userprofiles.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\plugins.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\login.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\logindefault.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\useredit.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\favicon.ico">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\touchicon.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\touchicon114.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\touchicon72.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\iossplash.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\myprofile.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\indexpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\items\list\collection.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\rightarrow.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\userflyoutdefault.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\medialibrarypage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\pluginspage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\loginpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\useredit.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\myprofile.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\userprofilespage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\addplugin.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\addpluginpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\plugincatalog.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\plugincatalogpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scheduledtasks.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\scheduledtaskspage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scheduledtask.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\scheduledtaskpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\wizardsettings.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\wizardstart.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\wizardfinish.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\wizarduser.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\wizardlibrary.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\wizardstartpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\wizarduserpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\log.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\dashboard\logpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\metadataimages.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\metadataimagespage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\dashboardpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\checkmarkgreen.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\clients\html5.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\clients\android.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\ios.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\clients\windowsrt.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\clients\windowsphone.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\supporter\supporterbadge.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\css\images\supporter\premiumflag.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\supporterkey.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\itemdetails.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\scripts\itemdetailpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="dashboard-ui\dashboard\aboutpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\supporter\supporterflag.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\scripts\itemlistpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\itemlist.html">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\dashboard\wizardfinishpage.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\detail\video.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\detail\audio.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\items\detail\game.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\mblogoicon.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\clients\dlna.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-  <ItemGroup>
-    <None Include="dashboard-ui\manifest.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.table.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.popup.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <None Include="dashboard-ui\strings\ar.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\be-by.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\bg-bg.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\ca.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\cs.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\da.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\de-de.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\de.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\el.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\en-gb.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\en-us.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\es-ar.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\es-es.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\es-mx.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\es.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\fi.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\fr-ca.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\fr-fr.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\fr.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\gsw.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\he.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\hr.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\hu.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\id.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\it.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\kk.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\ko.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\lt-lt.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\ms.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\nb.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\nl.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\pl.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\pt-br.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\pt-pt.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\ro.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\ru.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\sk.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\sl-si.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\sv.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\tr.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\uk.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\vi.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\zh-cn.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\zh-hk.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\strings\zh-tw.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup />

+ 0 - 11
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -394,17 +394,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                         break;
                     }
 
-                case "outline":
-                    {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            item.ShortOverview = val;
-                        }
-                        break;
-                    }
-
                 case "biography":
                 case "plot":
                 case "review":

+ 7 - 7
MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs

@@ -85,15 +85,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
             }
         }
 
-        protected override List<string> GetTagsUsed()
+        protected override List<string> GetTagsUsed(IHasMetadata item)
         {
-            var list = new List<string>
+            var list = base.GetTagsUsed(item);
+            list.AddRange(new string[]
             {
-                    "track",
-                    "artist",
-                    "albumartist"
-            };
-
+                "track",
+                "artist",
+                "albumartist"
+            });
             return list;
         }
 

+ 6 - 6
MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs

@@ -79,14 +79,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
             }
         }
 
-        protected override List<string> GetTagsUsed()
+        protected override List<string> GetTagsUsed(IHasMetadata item)
         {
-            var list = new List<string>
+            var list = base.GetTagsUsed(item);
+            list.AddRange(new string[]
             {
-                    "album",
-                    "disbanded"
-            };
-
+                "album",
+                "disbanded"
+            });
             return list;
         }
 

+ 20 - 5
MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs

@@ -183,9 +183,18 @@ namespace MediaBrowser.XbmcMetadata.Savers
         /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns>
         public abstract bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType);
 
-        protected virtual List<string> GetTagsUsed()
+        protected virtual List<string> GetTagsUsed(IHasMetadata item)
         {
-            return new List<string>();
+            var list = new List<string>();
+            foreach (var providerKey in item.ProviderIds.Keys)
+            {
+                var providerIdTagName = GetTagForProviderKey(providerKey);
+                if (!CommonTags.ContainsKey(providerIdTagName))
+                {
+                    list.Add(providerIdTagName);
+                }
+            }
+            return list;
         }
 
         public void Save(IHasMetadata item, CancellationToken cancellationToken)
@@ -271,7 +280,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
                     AddMediaInfo(hasMediaSources, writer);
                 }
 
-                var tagsUsed = GetTagsUsed();
+                var tagsUsed = GetTagsUsed(item);
 
                 try
                 {
@@ -457,7 +466,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
             if (item is Video)
             {
-                var outline = (item.ShortOverview ?? string.Empty)
+                var outline = (item.Tagline ?? string.Empty)
                     .StripHtml()
                     .Replace("&quot;", "'");
 
@@ -834,7 +843,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
                     var providerId = item.ProviderIds[providerKey];
                     if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
                     {
-                        writer.WriteElementString(providerKey.ToLower() + "id", providerId);
+                        writer.WriteElementString(GetTagForProviderKey(providerKey), providerId);
+                        writtenProviderIds.Add(providerKey);
                     }
                 }
             }
@@ -1093,5 +1103,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 }
             }
         }
+
+        private static string GetTagForProviderKey(string providerKey)
+        {
+            return providerKey.ToLower() + "id";
+        }
     }
 }

+ 16 - 16
MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs

@@ -108,24 +108,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
-        protected override List<string> GetTagsUsed()
+        protected override List<string> GetTagsUsed(IHasMetadata item)
         {
-            var list = new List<string>
+            var list = base.GetTagsUsed(item);
+            list.AddRange(new string[]
             {
-                    "aired",
-                    "season",
-                    "episode",
-                    "episodenumberend",
-                    "airsafter_season",
-                    "airsbefore_episode",
-                    "airsbefore_season",
-                    "DVD_episodenumber",
-                    "DVD_season",
-                    "absolute_number",
-                    "displayseason",
-                    "displayepisode"
-            };
-
+                "aired",
+                "season",
+                "episode",
+                "episodenumberend",
+                "airsafter_season",
+                "airsbefore_episode",
+                "airsbefore_season",
+                "DVD_episodenumber",
+                "DVD_season",
+                "absolute_number",
+                "displayseason",
+                "displayepisode"
+            });
             return list;
         }
 

+ 8 - 8
MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs

@@ -113,16 +113,16 @@ namespace MediaBrowser.XbmcMetadata.Savers
             }
         }
 
-        protected override List<string> GetTagsUsed()
+        protected override List<string> GetTagsUsed(IHasMetadata item)
         {
-            var list = new List<string>
+            var list = base.GetTagsUsed(item);
+            list.AddRange(new string[]
             {
-                    "album",
-                    "artist",
-                    "set",
-                    "id"
-            };
-
+                "album",
+                "artist",
+                "set",
+                "id"
+            });
             return list;
         }
 

+ 6 - 4
MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs

@@ -51,11 +51,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
             }
         }
 
-        protected override List<string> GetTagsUsed()
+        protected override List<string> GetTagsUsed(IHasMetadata item)
         {
-            var list = base.GetTagsUsed();
-
-            list.Add("seasonnumber");
+            var list = base.GetTagsUsed(item);
+            list.AddRange(new string[]
+            {
+                "seasonnumber"
+            });
 
             return list;
         }

+ 12 - 12
MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs

@@ -90,20 +90,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
             }
         }
 
-        protected override List<string> GetTagsUsed()
+        protected override List<string> GetTagsUsed(IHasMetadata item)
         {
-            var list = new List<string>
+            var list = base.GetTagsUsed(item);
+            list.AddRange(new string[]
             {
-                    "id",
-                    "episodeguide",
-                    "season",
-                    "episode",
-                    "status",
-                    "airs_time",
-                    "airs_dayofweek",
-                    "animeseriesindex"
-            };
-
+                "id",
+                "episodeguide",
+                "season",
+                "episode",
+                "status",
+                "airs_time",
+                "airs_dayofweek",
+                "animeseriesindex"
+            });
             return list;
         }
 

+ 3 - 5
RSSDP/ISsdpCommunicationsServer.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Net;
 
@@ -44,15 +45,12 @@ namespace Rssdp.Infrastructure
         /// <summary>
         /// Sends a message to a particular address (uni or multicast) and port.
         /// </summary>
-        /// <param name="messageData">A byte array containing the data to send.</param>
-        /// <param name="destination">A <see cref="IpEndPointInfo"/> representing the destination address for the data. Can be either a multicast or unicast destination.</param>
-        /// <param name="fromLocalIpAddress">A <see cref="IpEndPointInfo"/> The local ip address to send from, or .Any if sending from all available</param>
-        Task SendMessage(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress);
+        Task SendMessage(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
 
         /// <summary>
         /// Sends a message to the SSDP multicast address and port.
         /// </summary>
-        Task SendMulticastMessage(string message);
+        Task SendMulticastMessage(string message, CancellationToken cancellationToken);
 
         #endregion
 

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