Bladeren bron

Merge remote-tracking branch 'upstream/master' into FixFor5280Part2

BaronGreenback 4 jaren geleden
bovenliggende
commit
80ca3da55c
100 gewijzigde bestanden met toevoegingen van 2088 en 2540 verwijderingen
  1. 0 1
      Emby.Dlna/ContentDirectory/StubType.cs
  2. 1 1
      Emby.Dlna/DlnaManager.cs
  3. 10 4
      Emby.Dlna/Main/DlnaEntryPoint.cs
  4. 0 1
      Emby.Dlna/PlayTo/TransportState.cs
  5. 4 1
      Emby.Server.Implementations/ApplicationHost.cs
  6. 1 1
      Emby.Server.Implementations/Collections/CollectionManager.cs
  7. 3 3
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  8. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  9. 4 3
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  10. 16 7
      Emby.Server.Implementations/Library/LibraryManager.cs
  11. 2 2
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  12. 0 1
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  13. 0 5
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  14. 4 1
      Emby.Server.Implementations/Localization/Core/af.json
  15. 2 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  16. 51 1
      Emby.Server.Implementations/Localization/Core/gl.json
  17. 1 1
      Emby.Server.Implementations/Localization/Core/id.json
  18. 77 16
      Emby.Server.Implementations/Plugins/PluginManager.cs
  19. 0 1
      Emby.Server.Implementations/Session/WebSocketController.cs
  20. 5 17
      Emby.Server.Implementations/Updates/InstallationManager.cs
  21. 11 32
      Jellyfin.Api/Controllers/DashboardController.cs
  22. 2 2
      Jellyfin.Api/Controllers/ImageController.cs
  23. 1 1
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  24. 0 9
      Jellyfin.Api/Extensions/DtoExtensions.cs
  25. 7 4
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  26. 1 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  27. 2 2
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  28. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  29. 8 1
      Jellyfin.Api/Models/ConfigurationPageInfo.cs
  30. 1 1
      Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
  31. 23 14
      Jellyfin.Networking/Manager/NetworkManager.cs
  32. 0 1
      Jellyfin.Server/CoreAppHost.cs
  33. 1 1
      Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
  34. 51 0
      MediaBrowser.Common/Extensions/StreamExtensions.cs
  35. 0 37
      MediaBrowser.Common/Extensions/StringExtensions.cs
  36. 51 46
      MediaBrowser.Common/Net/IPHost.cs
  37. 0 5
      MediaBrowser.Common/Net/NetworkExtensions.cs
  38. 0 3
      MediaBrowser.Common/Plugins/BasePlugin.cs
  39. 10 0
      MediaBrowser.Common/Plugins/IPluginManager.cs
  40. 5 0
      MediaBrowser.Controller/IServerApplicationHost.cs
  41. 1 1
      MediaBrowser.Controller/Library/ILibraryManager.cs
  42. 4 3
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  43. 1 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  44. 3 3
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  45. 12 121
      MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
  46. 11 92
      MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
  47. 11 467
      MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
  48. 63 0
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
  49. 22 32
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  50. 9 9
      MediaBrowser.Model/Channels/ChannelFeatures.cs
  51. 3 3
      MediaBrowser.Model/Channels/ChannelQuery.cs
  52. 37 37
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  53. 5 5
      MediaBrowser.Model/Configuration/ImageOption.cs
  54. 19 384
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  55. 12 0
      MediaBrowser.Model/Configuration/MediaPathInfo.cs
  56. 2 2
      MediaBrowser.Model/Configuration/MetadataConfiguration.cs
  57. 10 10
      MediaBrowser.Model/Configuration/MetadataOptions.cs
  58. 6 6
      MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
  59. 365 0
      MediaBrowser.Model/Configuration/TypeOptions.cs
  60. 18 18
      MediaBrowser.Model/Configuration/UserConfiguration.cs
  61. 8 8
      MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
  62. 5 4
      MediaBrowser.Model/Dlna/AudioOptions.cs
  63. 6 6
      MediaBrowser.Model/Dlna/CodecProfile.cs
  64. 1 1
      MediaBrowser.Model/Dlna/ConditionProcessor.cs
  65. 5 5
      MediaBrowser.Model/Dlna/ContainerProfile.cs
  66. 19 17
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  67. 1 0
      MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
  68. 2 2
      MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
  69. 4 4
      MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
  70. 5 5
      MediaBrowser.Model/Dlna/ResponseProfile.cs
  71. 27 27
      MediaBrowser.Model/Dlna/SearchCriteria.cs
  72. 2 2
      MediaBrowser.Model/Dlna/SortCriteria.cs
  73. 8 6
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  74. 577 597
      MediaBrowser.Model/Dlna/StreamInfo.cs
  75. 5 5
      MediaBrowser.Model/Dto/BaseItemDto.cs
  76. 34 33
      MediaBrowser.Model/Dto/MediaSourceInfo.cs
  77. 9 9
      MediaBrowser.Model/Dto/MetadataEditorInfo.cs
  78. 14 0
      MediaBrowser.Model/Dto/NameGuidPair.cs
  79. 0 7
      MediaBrowser.Model/Dto/NameIdPair.cs
  80. 9 9
      MediaBrowser.Model/Dto/UserDto.cs
  81. 0 32
      MediaBrowser.Model/Entities/CollectionType.cs
  82. 16 0
      MediaBrowser.Model/Entities/CollectionTypeOptions.cs
  83. 98 99
      MediaBrowser.Model/Entities/MediaStream.cs
  84. 0 40
      MediaBrowser.Model/Entities/PackageReviewInfo.cs
  85. 35 26
      MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
  86. 36 0
      MediaBrowser.Model/Entities/SpecialFolder.cs
  87. 9 9
      MediaBrowser.Model/Entities/VirtualFolderInfo.cs
  88. 6 6
      MediaBrowser.Model/Globalization/CultureDto.cs
  89. 5 2
      MediaBrowser.Model/IO/IFileSystem.cs
  90. 8 8
      MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
  91. 58 0
      MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs
  92. 13 13
      MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
  93. 8 89
      MediaBrowser.Model/LiveTv/LiveTvOptions.cs
  94. 5 5
      MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
  95. 8 8
      MediaBrowser.Model/LiveTv/RecordingQuery.cs
  96. 8 8
      MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
  97. 38 0
      MediaBrowser.Model/LiveTv/TunerHostInfo.cs
  98. 2 2
      MediaBrowser.Model/MediaBrowser.Model.csproj
  99. 11 11
      MediaBrowser.Model/MediaInfo/MediaInfo.cs
  100. 11 11
      MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs

+ 0 - 1
Emby.Dlna/ContentDirectory/StubType.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1602
 
 
 namespace Emby.Dlna.ContentDirectory
 namespace Emby.Dlna.ContentDirectory
 {
 {

+ 1 - 1
Emby.Dlna/DlnaManager.cs

@@ -553,7 +553,7 @@ namespace Emby.Dlna
 
 
         private void DumpProfiles()
         private void DumpProfiles()
         {
         {
-            DeviceProfile[] list = new []
+            DeviceProfile[] list = new[]
             {
             {
                 new SamsungSmartTvProfile(),
                 new SamsungSmartTvProfile(),
                 new XboxOneProfile(),
                 new XboxOneProfile(),

+ 10 - 4
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -228,7 +228,10 @@ namespace Emby.Dlna.Main
         {
         {
             try
             try
             {
             {
-                ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+                if (communicationsServer != null)
+                {
+                    ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+                }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -313,9 +316,12 @@ namespace Emby.Dlna.Main
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
 
 
                 var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
                 var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
-                // DLNA will only work over http, so we must reset to http:// : {port}
-                uri.Scheme = "http://";
-                uri.Port = _netConfig.HttpServerPortNumber;
+                if (_appHost.PublishedServerUrl == null)
+                {
+                    // DLNA will only work over http, so we must reset to http:// : {port}.
+                    uri.Scheme = "http";
+                    uri.Port = _netConfig.HttpServerPortNumber;
+                }
 
 
                 var device = new SsdpRootDevice
                 var device = new SsdpRootDevice
                 {
                 {

+ 0 - 1
Emby.Dlna/PlayTo/TransportState.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1602
 
 
 namespace Emby.Dlna.PlayTo
 namespace Emby.Dlna.PlayTo
 {
 {

+ 4 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -137,6 +137,9 @@ namespace Emby.Server.Implementations
 
 
         public bool CoreStartupHasCompleted { get; private set; }
         public bool CoreStartupHasCompleted { get; private set; }
 
 
+        /// <inheritdoc />
+        public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl;
+
         public virtual bool CanLaunchWebBrowser
         public virtual bool CanLaunchWebBrowser
         {
         {
             get
             get
@@ -384,7 +387,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Creates an instance of type and resolves all constructor dependencies.
         /// Creates an instance of type and resolves all constructor dependencies.
         /// </summary>
         /// </summary>
-        /// /// <typeparam name="T">The type.</typeparam>
+        /// <typeparam name="T">The type.</typeparam>
         /// <returns>T.</returns>
         /// <returns>T.</returns>
         public T CreateInstance<T>()
         public T CreateInstance<T>()
             => ActivatorUtilities.CreateInstance<T>(ServiceProvider);
             => ActivatorUtilities.CreateInstance<T>(ServiceProvider);

+ 1 - 1
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Collections
 
 
             var name = _localizationManager.GetLocalizedString("Collections");
             var name = _localizationManager.GetLocalizedString("Collections");
 
 
-            await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
+            await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
 
 
             return FindFolders(path).First();
             return FindFolders(path).First();
         }
         }

+ 3 - 3
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -6207,9 +6207,9 @@ AND Type = @InternalPersonType)");
 
 
             if (item.Type == MediaStreamType.Subtitle)
             if (item.Type == MediaStreamType.Subtitle)
             {
             {
-                item.localizedUndefined = _localization.GetLocalizedString("Undefined");
-                item.localizedDefault = _localization.GetLocalizedString("Default");
-                item.localizedForced = _localization.GetLocalizedString("Forced");
+                item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+                item.LocalizedDefault = _localization.GetLocalizedString("Default");
+                item.LocalizedForced = _localization.GetLocalizedString("Forced");
             }
             }
 
 
             return item;
             return item;

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

@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
-    <PackageReference Include="sharpcompress" Version="0.28.0" />
+    <PackageReference Include="sharpcompress" Version="0.28.1" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
   </ItemGroup>
   </ItemGroup>

+ 4 - 3
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System.Net.Sockets;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -29,7 +31,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <summary>
         /// <summary>
         /// The UDP server.
         /// The UDP server.
         /// </summary>
         /// </summary>
-        private UdpServer _udpServer;
+        private UdpServer? _udpServer;
         private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
         private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
         private bool _disposed = false;
         private bool _disposed = false;
 
 
@@ -71,9 +73,8 @@ namespace Emby.Server.Implementations.EntryPoints
             }
             }
 
 
             _cancellationTokenSource.Cancel();
             _cancellationTokenSource.Cancel();
-            _udpServer.Dispose();
             _cancellationTokenSource.Dispose();
             _cancellationTokenSource.Dispose();
-            _cancellationTokenSource = null;
+            _udpServer?.Dispose();
             _udpServer = null;
             _udpServer = null;
 
 
             _disposed = true;
             _disposed = true;

+ 16 - 7
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1240,11 +1240,20 @@ namespace Emby.Server.Implementations.Library
             return info;
             return info;
         }
         }
 
 
-        private string GetCollectionType(string path)
+        private CollectionTypeOptions? GetCollectionType(string path)
         {
         {
-            return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
-                .Select(Path.GetFileNameWithoutExtension)
-                .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+            var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
+            foreach (var file in files)
+            {
+                // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
+                // https://github.com/dotnet/runtime/issues/20008
+                if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res))
+                {
+                    return res;
+                }
+            }
+
+            return null;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -2956,7 +2965,7 @@ namespace Emby.Server.Implementations.Library
             throw new InvalidOperationException();
             throw new InvalidOperationException();
         }
         }
 
 
-        public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
+        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
         {
         {
             if (string.IsNullOrWhiteSpace(name))
             if (string.IsNullOrWhiteSpace(name))
             {
             {
@@ -2990,9 +2999,9 @@ namespace Emby.Server.Implementations.Library
             {
             {
                 Directory.CreateDirectory(virtualFolderPath);
                 Directory.CreateDirectory(virtualFolderPath);
 
 
-                if (!string.IsNullOrEmpty(collectionType))
+                if (collectionType != null)
                 {
                 {
-                    var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
+                    var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection");
 
 
                     File.WriteAllBytes(path, Array.Empty<byte>());
                     File.WriteAllBytes(path, Array.Empty<byte>());
                 }
                 }

+ 2 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -2604,7 +2604,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                 {
                     Locations = new string[] { customPath },
                     Locations = new string[] { customPath },
                     Name = "Recorded Movies",
                     Name = "Recorded Movies",
-                    CollectionType = CollectionType.Movies
+                    CollectionType = CollectionTypeOptions.Movies
                 };
                 };
             }
             }
 
 
@@ -2615,7 +2615,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                 {
                     Locations = new string[] { customPath },
                     Locations = new string[] { customPath },
                     Name = "Recorded Shows",
                     Name = "Recorded Shows",
-                    CollectionType = CollectionType.TvShows
+                    CollectionType = CollectionTypeOptions.TvShows
                 };
                 };
             }
             }
         }
         }

+ 0 - 1
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -10,7 +10,6 @@ using System.Net.Http;
 using System.Net.Mime;
 using System.Net.Mime;
 using System.Text;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json;
-using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;

+ 0 - 5
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -335,11 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             return new Uri(url).AbsoluteUri.TrimEnd('/');
             return new Uri(url).AbsoluteUri.TrimEnd('/');
         }
         }
 
 
-        protected EncodingOptions GetEncodingOptions()
-        {
-            return Config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
         private static string GetHdHrIdFromChannelId(string channelId)
         private static string GetHdHrIdFromChannelId(string channelId)
         {
         {
             return channelId.Split('_')[1];
             return channelId.Split('_')[1];

+ 4 - 1
Emby.Server.Implementations/Localization/Core/af.json

@@ -112,5 +112,8 @@
     "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
     "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
     "TaskRefreshLibrary": "Skandeer Media Versameling",
     "TaskRefreshLibrary": "Skandeer Media Versameling",
     "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
     "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
-    "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
+    "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde",
+    "Undefined": "Ongedefineerd",
+    "Forced": "Geforseer",
+    "Default": "Oorspronklik"
 }
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -117,5 +117,6 @@
     "TaskCleanActivityLogDescription": "Elimina entradas del registro de actividad que sean más antiguas al periodo establecido.",
     "TaskCleanActivityLogDescription": "Elimina entradas del registro de actividad que sean más antiguas al periodo establecido.",
     "TaskCleanActivityLog": "Limpiar registro de actividades",
     "TaskCleanActivityLog": "Limpiar registro de actividades",
     "Undefined": "Sin definir",
     "Undefined": "Sin definir",
-    "Forced": "Forzado"
+    "Forced": "Forzado",
+    "Default": "Predeterminado"
 }
 }

+ 51 - 1
Emby.Server.Implementations/Localization/Core/gl.json

@@ -7,5 +7,55 @@
     "Books": "Libros",
     "Books": "Libros",
     "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
     "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
     "Artists": "Artistas",
     "Artists": "Artistas",
-    "Application": "Aplicativo"
+    "Application": "Aplicativo",
+    "NotificationOptionServerRestartRequired": "Necesario un reinicio do servidor",
+    "NotificationOptionPluginUpdateInstalled": "Actualización do Plugin instalada",
+    "NotificationOptionPluginUninstalled": "Plugin desinstalado",
+    "NotificationOptionPluginInstalled": "Plugin instalado",
+    "NotificationOptionPluginError": "Fallo do Plugin",
+    "NotificationOptionNewLibraryContent": "Novo contido engadido",
+    "NotificationOptionInstallationFailed": "Fallo na instalación",
+    "NotificationOptionCameraImageUploaded": "Imaxe da cámara subida",
+    "NotificationOptionAudioPlaybackStopped": "Reproducción de audio parada",
+    "NotificationOptionAudioPlayback": "Reproducción de audio comezada",
+    "NotificationOptionApplicationUpdateInstalled": "Actualización da aplicación instalada",
+    "NotificationOptionApplicationUpdateAvailable": "Actualización da aplicación dispoñible",
+    "NewVersionIsAvailable": "Unha nova versión do Servidor Jellyfin está dispoñible para descarga.",
+    "NameSeasonUnknown": "Tempada descoñecida",
+    "NameSeasonNumber": "Tempada {0}",
+    "NameInstallFailed": "{0} instalación fallida",
+    "MusicVideos": "Vídeos Musicais",
+    "Music": "Música",
+    "Movies": "Películas",
+    "MixedContent": "Contido Mixto",
+    "MessageServerConfigurationUpdated": "A configuración do servidor foi actualizada",
+    "MessageNamedServerConfigurationUpdatedWithValue": "A sección de configuración {0} do servidor foi actualizada",
+    "MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado a {0}",
+    "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
+    "Latest": "Último",
+    "LabelRunningTimeValue": "Tempo de execución: {0}",
+    "LabelIpAddressValue": "Enderezo IP: {0}",
+    "ItemRemovedWithName": "{0} foi eliminado da biblioteca",
+    "ItemAddedWithName": "{0} foi engadido a biblioteca",
+    "Inherit": "Herdar",
+    "HomeVideos": "Videos caseiros",
+    "HeaderRecordingGroups": "Grupos de Grabación",
+    "HeaderNextUp": "De seguido",
+    "HeaderLiveTV": "TV en directo",
+    "HeaderFavoriteSongs": "Cancións Favoritas",
+    "HeaderFavoriteShows": "Series de TV Favoritas",
+    "HeaderFavoriteEpisodes": "Episodios Favoritos",
+    "HeaderFavoriteArtists": "Artistas Favoritos",
+    "HeaderFavoriteAlbums": "Álbunes Favoritos",
+    "HeaderContinueWatching": "Seguir mirando",
+    "HeaderAlbumArtists": "Artistas de Album",
+    "Genres": "Xéneros",
+    "Forced": "Forzado",
+    "Folders": "Cartafoles",
+    "Favorites": "Favoritos",
+    "FailedLoginAttemptWithUserName": "Intento de incio de sesión fallido {0}",
+    "DeviceOnlineWithName": "{0} conectouse",
+    "DeviceOfflineWithName": "{0} desconectouse",
+    "Default": "Por defecto",
+    "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}"
 }
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/id.json

@@ -1,7 +1,7 @@
 {
 {
     "Albums": "Album",
     "Albums": "Album",
     "AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi",
     "AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi",
-    "AppDeviceValues": "Aplikasi : {0}, Alat : {1}",
+    "AppDeviceValues": "Aplikasi : {0}, Perangkat : {1}",
     "LabelRunningTimeValue": "Waktu berjalan: {0}",
     "LabelRunningTimeValue": "Waktu berjalan: {0}",
     "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}",
     "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}",
     "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
     "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",

+ 77 - 16
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -1,8 +1,10 @@
 #nullable enable
 #nullable enable
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Net.Http;
 using System.Reflection;
 using System.Reflection;
 using System.Text;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json;
@@ -11,9 +13,11 @@ using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Json.Converters;
 using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -33,6 +37,21 @@ namespace Emby.Server.Implementations.Plugins
         private readonly IList<LocalPlugin> _plugins;
         private readonly IList<LocalPlugin> _plugins;
         private readonly Version _minimumVersion;
         private readonly Version _minimumVersion;
 
 
+        private IHttpClientFactory? _httpClientFactory;
+
+        private IHttpClientFactory HttpClientFactory
+        {
+            get
+            {
+                if (_httpClientFactory == null)
+                {
+                    _httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
+                }
+
+                return _httpClientFactory;
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="PluginManager"/> class.
         /// Initializes a new instance of the <see cref="PluginManager"/> class.
         /// </summary>
         /// </summary>
@@ -332,32 +351,74 @@ namespace Emby.Server.Implementations.Plugins
             ChangePluginState(plugin, PluginStatus.Malfunctioned);
             ChangePluginState(plugin, PluginStatus.Malfunctioned);
         }
         }
 
 
-        /// <summary>
-        /// Saves the manifest back to disk.
-        /// </summary>
-        /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
-        /// <param name="path">The path where to save the manifest.</param>
-        /// <returns>True if successful.</returns>
+        /// <inheritdoc/>
         public bool SaveManifest(PluginManifest manifest, string path)
         public bool SaveManifest(PluginManifest manifest, string path)
         {
         {
-            if (manifest == null)
-            {
-                return false;
-            }
-
             try
             try
             {
             {
                 var data = JsonSerializer.Serialize(manifest, _jsonOptions);
                 var data = JsonSerializer.Serialize(manifest, _jsonOptions);
                 File.WriteAllText(Path.Combine(path, "meta.json"), data);
                 File.WriteAllText(Path.Combine(path, "meta.json"), data);
                 return true;
                 return true;
             }
             }
-#pragma warning disable CA1031 // Do not catch general exception types
-            catch (Exception e)
-#pragma warning restore CA1031 // Do not catch general exception types
+            catch (ArgumentException e)
+            {
+                _logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
+                return false;
+            }
+        }
+
+        /// <inheritdoc/>
+        public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
+        {
+            if (packageInfo == null)
             {
             {
-                _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
                 return false;
                 return false;
             }
             }
+
+            var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
+            var imagePath = string.Empty;
+
+            if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
+            {
+                var url = new Uri(packageInfo.ImageUrl);
+                imagePath = Path.Join(path, url.Segments[^1]);
+
+                await using var fileStream = File.OpenWrite(imagePath);
+
+                try
+                {
+                    await using var downloadStream = await HttpClientFactory
+                        .CreateClient(NamedClient.Default)
+                        .GetStreamAsync(url)
+                        .ConfigureAwait(false);
+
+                    await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
+                }
+                catch (HttpRequestException ex)
+                {
+                    _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
+                    imagePath = string.Empty;
+                }
+            }
+
+            var manifest = new PluginManifest
+            {
+                Category = packageInfo.Category,
+                Changelog = versionInfo.Changelog ?? string.Empty,
+                Description = packageInfo.Description,
+                Id = new Guid(packageInfo.Id),
+                Name = packageInfo.Name,
+                Overview = packageInfo.Overview,
+                Owner = packageInfo.Owner,
+                TargetAbi = versionInfo.TargetAbi ?? string.Empty,
+                Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp),
+                Version = versionInfo.Version,
+                Status = PluginStatus.Active,
+                AutoUpdate = true,
+                ImagePath = imagePath
+            };
+
+            return SaveManifest(manifest, path);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -410,7 +471,7 @@ namespace Emby.Server.Implementations.Plugins
                 if (plugin == null)
                 if (plugin == null)
                 {
                 {
                     // Create a dummy record for the providers.
                     // Create a dummy record for the providers.
-                    // TODO: remove this code, if all provided have been released as separate plugins.
+                    // TODO: remove this code once all provided have been released as separate plugins.
                     plugin = new LocalPlugin(
                     plugin = new LocalPlugin(
                         instance.AssemblyFilePath,
                         instance.AssemblyFilePath,
                         true,
                         true,

+ 0 - 1
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -8,7 +8,6 @@ using System.Linq;
 using System.Net.WebSockets;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;

+ 5 - 17
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -192,17 +192,12 @@ namespace Emby.Server.Implementations.Updates
                             var version = package.Versions[i];
                             var version = package.Versions[i];
 
 
                             var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
                             var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
-                            // Update the manifests, if anything changes.
                             if (plugin != null)
                             if (plugin != null)
                             {
                             {
-                                if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
-                                {
-                                    plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
-                                    _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
-                                }
+                                await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path);
                             }
                             }
 
 
-                            // Remove versions with a target abi that is greater then the current application version.
+                            // Remove versions with a target ABI greater then the current application version.
                             if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
                             if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
                             {
                             {
                                 package.Versions.RemoveAt(i);
                                 package.Versions.RemoveAt(i);
@@ -294,7 +289,8 @@ namespace Emby.Server.Implementations.Updates
                     Name = package.Name,
                     Name = package.Name,
                     Version = v.VersionNumber,
                     Version = v.VersionNumber,
                     SourceUrl = v.SourceUrl,
                     SourceUrl = v.SourceUrl,
-                    Checksum = v.Checksum
+                    Checksum = v.Checksum,
+                    PackageInfo = package
                 };
                 };
             }
             }
         }
         }
@@ -571,24 +567,16 @@ namespace Emby.Server.Implementations.Updates
 
 
             stream.Position = 0;
             stream.Position = 0;
             _zipClient.ExtractAllFromZip(stream, targetDir, true);
             _zipClient.ExtractAllFromZip(stream, targetDir, true);
+            await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
             _pluginManager.ImportPluginFrom(targetDir);
             _pluginManager.ImportPluginFrom(targetDir);
         }
         }
 
 
         private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
         private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
         {
         {
-            // Set last update time if we were installed before
             LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
             LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
                   ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
                   ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
-            if (plugin != null)
-            {
-                plugin.Manifest.Timestamp = DateTime.UtcNow;
-                _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
-            }
 
 
-            // Do the install
             await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
             await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
-
-            // Do plugin-specific processing
             _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
             _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
 
 
             return plugin != null;
             return plugin != null;

+ 11 - 32
Jellyfin.Api/Controllers/DashboardController.cs

@@ -6,7 +6,6 @@ using System.Net.Mime;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Models;
 using Jellyfin.Api.Models;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
@@ -22,22 +21,18 @@ namespace Jellyfin.Api.Controllers
     public class DashboardController : BaseJellyfinApiController
     public class DashboardController : BaseJellyfinApiController
     {
     {
         private readonly ILogger<DashboardController> _logger;
         private readonly ILogger<DashboardController> _logger;
-        private readonly IServerApplicationHost _appHost;
         private readonly IPluginManager _pluginManager;
         private readonly IPluginManager _pluginManager;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// </summary>
         /// </summary>
         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
-        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
         /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
         /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
         public DashboardController(
         public DashboardController(
             ILogger<DashboardController> logger,
             ILogger<DashboardController> logger,
-            IServerApplicationHost appHost,
             IPluginManager pluginManager)
             IPluginManager pluginManager)
         {
         {
             _logger = logger;
             _logger = logger;
-            _appHost = appHost;
             _pluginManager = pluginManager;
             _pluginManager = pluginManager;
         }
         }
 
 
@@ -51,7 +46,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("web/ConfigurationPages")]
         [HttpGet("web/ConfigurationPages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
+        public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
             [FromQuery] bool? enableInMainMenu)
             [FromQuery] bool? enableInMainMenu)
         {
         {
             var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
             var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
@@ -77,38 +72,22 @@ namespace Jellyfin.Api.Controllers
         [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
         [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
         public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
         public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
         {
         {
-            IPlugin? plugin = null;
-            Stream? stream = null;
-
-            var isJs = false;
-            var isTemplate = false;
-
             var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
             var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
-            if (altPage != null)
+            if (altPage == null)
             {
             {
-                plugin = altPage.Item2;
-                stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
-
-                isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
-                isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
+                return NotFound();
             }
             }
 
 
-            if (plugin != null && stream != null)
+            IPlugin plugin = altPage.Item2;
+            string resourcePath = altPage.Item1.EmbeddedResourcePath;
+            Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
+            if (stream == null)
             {
             {
-                if (isJs)
-                {
-                    return File(stream, MimeTypes.GetMimeType("page.js"));
-                }
-
-                if (isTemplate)
-                {
-                    return File(stream, MimeTypes.GetMimeType("page.html"));
-                }
-
-                return File(stream, MimeTypes.GetMimeType("page.html"));
+                _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
+                return NotFound();
             }
             }
 
 
-            return NotFound();
+            return File(stream, MimeTypes.GetMimeType(resourcePath));
         }
         }
 
 
         private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
         private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
@@ -120,7 +99,7 @@ namespace Jellyfin.Api.Controllers
         {
         {
             if (plugin?.Instance is not IHasWebPages hasWebPages)
             if (plugin?.Instance is not IHasWebPages hasWebPages)
             {
             {
-                return new List<Tuple<PluginPageInfo, IPlugin>>();
+                return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
             }
             }
 
 
             return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
             return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));

+ 2 - 2
Jellyfin.Api/Controllers/ImageController.cs

@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
                 await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
                 await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
             }
             }
 
 
-            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
 
 
             await _providerManager
             await _providerManager
                 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
                 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
                 await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
                 await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
             }
             }
 
 
-            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
 
 
             await _providerManager
             await _providerManager
                 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
                 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)

+ 1 - 1
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> AddVirtualFolder(
         public async Task<ActionResult> AddVirtualFolder(
             [FromQuery] string? name,
             [FromQuery] string? name,
-            [FromQuery] string? collectionType,
+            [FromQuery] CollectionTypeOptions? collectionType,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
             [FromBody] AddVirtualFolderDto? libraryOptionsDto,
             [FromBody] AddVirtualFolderDto? libraryOptionsDto,
             [FromQuery] bool refreshLibrary = false)
             [FromQuery] bool refreshLibrary = false)

+ 0 - 9
Jellyfin.Api/Extensions/DtoExtensions.cs

@@ -113,14 +113,5 @@ namespace Jellyfin.Api.Extensions
 
 
             return dtoOptions;
             return dtoOptions;
         }
         }
-
-        /// <summary>
-        /// Check if DtoOptions contains field.
-        /// </summary>
-        /// <param name="dtoOptions">DtoOptions object.</param>
-        /// <param name="field">Field to check.</param>
-        /// <returns>Field existence.</returns>
-        internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field)
-            => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field);
     }
     }
 }
 }

+ 7 - 4
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
+using System.Net.Mime;
 using System.Security.Claims;
 using System.Security.Claims;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
@@ -171,13 +172,15 @@ namespace Jellyfin.Api.Helpers
             var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
             var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
 
 
             // from universal audio service
             // from universal audio service
-            if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
+            if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
+                && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
             {
             {
                 queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
                 queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
             }
             }
 
 
             // from universal audio service
             // from universal audio service
-            if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
+            if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
+                && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
             {
             {
                 queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
                 queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
             }
             }
@@ -560,13 +563,13 @@ namespace Jellyfin.Api.Helpers
                 profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
                 profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
                 if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    profileString = profileString ?? "high";
+                    profileString ??= "high";
                 }
                 }
 
 
                 if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
                 if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
                     || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    profileString = profileString ?? "main";
+                    profileString ??= "main";
                 }
                 }
             }
             }
 
 

+ 1 - 1
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -523,7 +523,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="type">Dlna profile type.</param>
         /// <param name="type">Dlna profile type.</param>
         public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
         public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
         {
         {
-            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
+            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
         }
         }
 
 
         private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
         private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)

+ 2 - 2
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -183,7 +183,7 @@ namespace Jellyfin.Api.Helpers
             if (string.IsNullOrEmpty(containerInternal))
             if (string.IsNullOrEmpty(containerInternal))
             {
             {
                 containerInternal = streamingRequest.Static ?
                 containerInternal = streamingRequest.Static ?
-                    StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio)
+                    StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
                     : GetOutputFileExtension(state);
                     : GetOutputFileExtension(state);
             }
             }
 
 
@@ -245,7 +245,7 @@ namespace Jellyfin.Api.Helpers
 
 
             var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
             var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
                 ? GetOutputFileExtension(state)
                 ? GetOutputFileExtension(state)
-                : ('.' + state.OutputContainer);
+                : ("." + state.OutputContainer);
 
 
             state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
             state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
 
 

+ 2 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -17,8 +17,8 @@
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.5" />
-    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.5" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.7" />
+    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.7" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 8 - 1
Jellyfin.Api/Models/ConfigurationPageInfo.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Plugins;
 
 
 namespace Jellyfin.Api.Models
 namespace Jellyfin.Api.Models
@@ -25,6 +24,14 @@ namespace Jellyfin.Api.Models
             PluginId = plugin?.Id;
             PluginId = plugin?.Id;
         }
         }
 
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
+        /// </summary>
+        public ConfigurationPageInfo()
+        {
+            Name = string.Empty;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>

+ 1 - 1
Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs

@@ -98,7 +98,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
 
 
         private EncodingOptions GetOptions()
         private EncodingOptions GetOptions()
         {
         {
-            return _config.GetConfiguration<EncodingOptions>("encoding");
+            return _config.GetEncodingOptions();
         }
         }
 
 
         private async void TimerCallback(object? state)
         private async void TimerCallback(object? state)

+ 23 - 14
Jellyfin.Networking/Manager/NetworkManager.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
@@ -691,11 +692,11 @@ namespace Jellyfin.Networking.Manager
         /// Checks the string to see if it matches any interface names.
         /// Checks the string to see if it matches any interface names.
         /// </summary>
         /// </summary>
         /// <param name="token">String to check.</param>
         /// <param name="token">String to check.</param>
-        /// <param name="index">Interface index number.</param>
+        /// <param name="index">Interface index numbers that match.</param>
         /// <returns><c>true</c> if an interface name matches the token, <c>False</c> otherwise.</returns>
         /// <returns><c>true</c> if an interface name matches the token, <c>False</c> otherwise.</returns>
-        private bool IsInterface(string token, out int index)
+        private bool TryGetInterfaces(string token, [NotNullWhen(true)] out List<int>? index)
         {
         {
-            index = -1;
+            index = null;
 
 
             // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
             // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
             // Null check required here for automated testing.
             // Null check required here for automated testing.
@@ -712,13 +713,13 @@ namespace Jellyfin.Networking.Manager
                     if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase))
                     if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase))
                         || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture)))
                         || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture)))
                     {
                     {
-                        index = interfcIndex;
-                        return true;
+                        index ??= new List<int>();
+                        index.Add(interfcIndex);
                     }
                     }
                 }
                 }
             }
             }
 
 
-            return false;
+            return index != null;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -730,14 +731,14 @@ namespace Jellyfin.Networking.Manager
         {
         {
             // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
             // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
             // Null check required here for automated testing.
             // Null check required here for automated testing.
-            if (IsInterface(token, out int index))
+            if (TryGetInterfaces(token, out var indices))
             {
             {
                 _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token);
                 _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token);
 
 
-                // Replace interface tags with the interface IP's.
+                // Replace all the interface tags with the interface IP's.
                 foreach (IPNetAddress iface in _interfaceAddresses)
                 foreach (IPNetAddress iface in _interfaceAddresses)
                 {
                 {
-                    if (Math.Abs(iface.Tag) == index
+                    if (indices.Contains(Math.Abs(iface.Tag))
                         && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
                         && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
                             || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
                             || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
                     {
                     {
@@ -916,11 +917,19 @@ namespace Jellyfin.Networking.Manager
                 // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
                 // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
                 if (config.IgnoreVirtualInterfaces)
                 if (config.IgnoreVirtualInterfaces)
                 {
                 {
-                    var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',');
-                    var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
-                    Array.Copy(lanAddresses, newList, lanAddresses.Length);
-                    Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
-                    lanAddresses = newList;
+                    // each virtual interface name must be pre-pended with the exclusion symbol !
+                    var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray();
+                    if (lanAddresses.Length > 0)
+                    {
+                        var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
+                        Array.Copy(lanAddresses, newList, lanAddresses.Length);
+                        Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
+                        lanAddresses = newList;
+                    }
+                    else
+                    {
+                        lanAddresses = virtualInterfaceNames;
+                    }
                 }
                 }
 
 
                 // Read and parse bind addresses and exclusions, removing ones that don't exist.
                 // Read and parse bind addresses and exclusions, removing ones that don't exist.

+ 0 - 1
Jellyfin.Server/CoreAppHost.cs

@@ -11,7 +11,6 @@ using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Events;
 using Jellyfin.Server.Implementations.Events;
 using Jellyfin.Server.Implementations.Users;
 using Jellyfin.Server.Implementations.Users;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.BaseItemManager;
 using MediaBrowser.Controller.BaseItemManager;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;

+ 1 - 1
Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs

@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Migrations.Routines
         public void Perform()
         public void Perform()
         {
         {
             // Set EnableThrottling to false since it wasn't used before and may introduce issues
             // Set EnableThrottling to false since it wasn't used before and may introduce issues
-            var encoding = _configManager.GetConfiguration<EncodingOptions>("encoding");
+            var encoding = _configManager.GetEncodingOptions();
             if (encoding.EnableThrottling)
             if (encoding.EnableThrottling)
             {
             {
                 _logger.LogInformation("Disabling transcoding throttling during migration");
                 _logger.LogInformation("Disabling transcoding throttling during migration");

+ 51 - 0
MediaBrowser.Common/Extensions/StreamExtensions.cs

@@ -0,0 +1,51 @@
+#nullable enable
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Class BaseExtensions.
+    /// </summary>
+    public static class StreamExtensions
+    {
+        /// <summary>
+        /// Reads all lines in the <see cref="Stream" />.
+        /// </summary>
+        /// <param name="stream">The <see cref="Stream" /> to read from.</param>
+        /// <returns>All lines in the stream.</returns>
+        public static string[] ReadAllLines(this Stream stream)
+            => ReadAllLines(stream, Encoding.UTF8);
+
+        /// <summary>
+        /// Reads all lines in the <see cref="Stream" />.
+        /// </summary>
+        /// <param name="stream">The <see cref="Stream" /> to read from.</param>
+        /// <param name="encoding">The character encoding to use.</param>
+        /// <returns>All lines in the stream.</returns>
+        public static string[] ReadAllLines(this Stream stream, Encoding encoding)
+        {
+            using (StreamReader reader = new StreamReader(stream, encoding))
+            {
+                return ReadAllLines(reader).ToArray();
+            }
+        }
+
+        /// <summary>
+        /// Reads all lines in the <see cref="StreamReader" />.
+        /// </summary>
+        /// <param name="reader">The <see cref="StreamReader" /> to read from.</param>
+        /// <returns>All lines in the stream.</returns>
+        public static IEnumerable<string> ReadAllLines(this StreamReader reader)
+        {
+            string? line;
+            while ((line = reader.ReadLine()) != null)
+            {
+                yield return line;
+            }
+        }
+    }
+}

+ 0 - 37
MediaBrowser.Common/Extensions/StringExtensions.cs

@@ -1,37 +0,0 @@
-#nullable enable
-
-using System;
-
-namespace MediaBrowser.Common.Extensions
-{
-    /// <summary>
-    /// Extensions methods to simplify string operations.
-    /// </summary>
-    public static class StringExtensions
-    {
-        /// <summary>
-        /// Returns the part on the left of the <c>needle</c>.
-        /// </summary>
-        /// <param name="haystack">The string to seek.</param>
-        /// <param name="needle">The needle to find.</param>
-        /// <returns>The part left of the <paramref name="needle" />.</returns>
-        public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle)
-        {
-            var pos = haystack.IndexOf(needle);
-            return pos == -1 ? haystack : haystack[..pos];
-        }
-
-        /// <summary>
-        /// Returns the part on the left of the <c>needle</c>.
-        /// </summary>
-        /// <param name="haystack">The string to seek.</param>
-        /// <param name="needle">The needle to find.</param>
-        /// <param name="stringComparison">One of the enumeration values that specifies the rules for the search.</param>
-        /// <returns>The part left of the <c>needle</c>.</returns>
-        public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, StringComparison stringComparison = default)
-        {
-            var pos = haystack.IndexOf(needle, stringComparison);
-            return pos == -1 ? haystack : haystack[..pos];
-        }
-    }
-}

+ 51 - 46
MediaBrowser.Common/Net/IPHost.cs

@@ -128,62 +128,63 @@ namespace MediaBrowser.Common.Net
         /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
         /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
         public static bool TryParse(string host, out IPHost hostObj)
         public static bool TryParse(string host, out IPHost hostObj)
         {
         {
-            if (!string.IsNullOrEmpty(host))
+            if (string.IsNullOrWhiteSpace(host))
             {
             {
-                // See if it's an IPv6 with port address e.g. [::1]:120.
-                int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase);
-                if (i != -1)
-                {
-                    return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
-                }
-                else
-                {
-                    // See if it's an IPv6 in [] with no port.
-                    i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase);
-                    if (i != -1)
-                    {
-                        return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
-                    }
+                hostObj = IPHost.None;
+                return false;
+            }
 
 
-                    // Is it a host or IPv4 with port?
-                    string[] hosts = host.Split(':');
+            // See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
+            int i = host.IndexOf("]", StringComparison.OrdinalIgnoreCase);
+            if (i != -1)
+            {
+                return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
+            }
 
 
-                    if (hosts.Length > 2)
-                    {
-                        hostObj = new IPHost(string.Empty, IPAddress.None);
-                        return false;
-                    }
+            if (IPNetAddress.TryParse(host, out var netAddress))
+            {
+                // Host name is an ip address, so fake resolve.
+                hostObj = new IPHost(host, netAddress.Address);
+                return true;
+            }
 
 
-                    // Remove port from IPv4 if it exists.
-                    host = hosts[0];
+            // Is it a host, IPv4/6 with/out port?
+            string[] hosts = host.Split(':');
 
 
-                    if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
-                    {
-                        hostObj = new IPHost(host, new IPAddress(Ipv4Loopback));
-                        return true;
-                    }
+            if (hosts.Length <= 2)
+            {
+                // This is either a hostname: port, or an IP4:port.
+                host = hosts[0];
 
 
-                    if (IPNetAddress.TryParse(host, out IPNetAddress netIP))
-                    {
-                        // Host name is an ip address, so fake resolve.
-                        hostObj = new IPHost(host, netIP.Address);
-                        return true;
-                    }
+                if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
+                {
+                    hostObj = new IPHost(host);
+                    return true;
                 }
                 }
 
 
-                // Only thing left is to see if it's a host string.
-                if (!string.IsNullOrEmpty(host))
+                if (IPAddress.TryParse(host, out var netIP))
                 {
                 {
-                    // Use regular expression as CheckHostName isn't RFC5892 compliant.
-                    // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
-                    Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
-                    if (re.Match(host).Success)
-                    {
-                        hostObj = new IPHost(host);
-                        return true;
-                    }
+                    // Host name is an ip address, so fake resolve.
+                    hostObj = new IPHost(host, netIP);
+                    return true;
                 }
                 }
             }
             }
+            else
+            {
+                // Invalid host name, as it cannot contain :
+                hostObj = new IPHost(string.Empty, IPAddress.None);
+                return false;
+            }
+
+            // Use regular expression as CheckHostName isn't RFC5892 compliant.
+            // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
+            string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$";
+
+            if (Regex.IsMatch(host, pattern))
+            {
+                hostObj = new IPHost(host);
+                return true;
+            }
 
 
             hostObj = IPHost.None;
             hostObj = IPHost.None;
             return false;
             return false;
@@ -344,10 +345,14 @@ namespace MediaBrowser.Common.Net
                     {
                     {
                         output += "Any Address,";
                         output += "Any Address,";
                     }
                     }
-                    else
+                    else if (i.AddressFamily == AddressFamily.InterNetwork)
                     {
                     {
                         output += $"{i}/32,";
                         output += $"{i}/32,";
                     }
                     }
+                    else
+                    {
+                        output += $"{i}/128,";
+                    }
                 }
                 }
 
 
                 output = output[0..^1];
                 output = output[0..^1];

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

@@ -1,11 +1,6 @@
-#pragma warning disable CA1062 // Validate arguments of public methods
 using System;
 using System;
-using System.Collections;
-using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.Net;
 using System.Net;
-using System.Runtime.CompilerServices;
-using System.Text;
 
 
 namespace MediaBrowser.Common.Net
 namespace MediaBrowser.Common.Net
 {
 {

+ 0 - 3
MediaBrowser.Common/Plugins/BasePlugin.cs

@@ -1,10 +1,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Reflection;
 using System.Reflection;
-using System.Runtime.InteropServices;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
 
 
 namespace MediaBrowser.Common.Plugins
 namespace MediaBrowser.Common.Plugins
 {
 {

+ 10 - 0
MediaBrowser.Common/Plugins/IPluginManager.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Reflection;
 using System.Reflection;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 
 
 namespace MediaBrowser.Common.Plugins
 namespace MediaBrowser.Common.Plugins
@@ -44,6 +45,15 @@ namespace MediaBrowser.Common.Plugins
         /// <returns>True if successful.</returns>
         /// <returns>True if successful.</returns>
         bool SaveManifest(PluginManifest manifest, string path);
         bool SaveManifest(PluginManifest manifest, string path);
 
 
+        /// <summary>
+        /// Generates a manifest from repository data.
+        /// </summary>
+        /// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param>
+        /// <param name="version">Version to be installed.</param>
+        /// <param name="path">The path where to save the manifest.</param>
+        /// <returns>True if successful.</returns>
+        Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path);
+
         /// <summary>
         /// <summary>
         /// Imports plugin details from a folder.
         /// Imports plugin details from a folder.
         /// </summary>
         /// </summary>

+ 5 - 0
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -52,6 +52,11 @@ namespace MediaBrowser.Controller
         /// <value>The name of the friendly.</value>
         /// <value>The name of the friendly.</value>
         string FriendlyName { get; }
         string FriendlyName { get; }
 
 
+        /// <summary>
+        /// Gets the configured published server url.
+        /// </summary>
+        Uri PublishedServerUrl { get; }
+
         /// <summary>
         /// <summary>
         /// Gets the system info.
         /// Gets the system info.
         /// </summary>
         /// </summary>

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

@@ -542,7 +542,7 @@ namespace MediaBrowser.Controller.Library
 
 
         Guid GetMusicGenreId(string name);
         Guid GetMusicGenreId(string name);
 
 
-        Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary);
+        Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary);
 
 
         Task RemoveVirtualFolder(string name, bool refreshLibrary);
         Task RemoveVirtualFolder(string name, bool refreshLibrary);
 
 

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

@@ -103,7 +103,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         public void SetFFmpegPath()
         public void SetFFmpegPath()
         {
         {
             // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
             // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
-            if (!ValidatePath(_configurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
+            if (!ValidatePath(_configurationManager.GetEncodingOptions().EncoderAppPath, FFmpegLocation.Custom))
             {
             {
                 // 2) Check if the --ffmpeg CLI switch has been given
                 // 2) Check if the --ffmpeg CLI switch has been given
                 if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
                 if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
@@ -118,7 +118,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
             }
 
 
             // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
             // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
-            var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
+            var config = _configurationManager.GetEncodingOptions();
             config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
             config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
             _configurationManager.SaveConfiguration("encoding", config);
             _configurationManager.SaveConfiguration("encoding", config);
 
 
@@ -177,7 +177,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
             // Write the new ffmpeg path to the xml as <EncoderAppPath>
             // Write the new ffmpeg path to the xml as <EncoderAppPath>
             // This ensures its not lost on next startup
             // This ensures its not lost on next startup
-            var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
+            var config = _configurationManager.GetEncodingOptions();
             config.EncoderAppPath = newPath;
             config.EncoderAppPath = newPath;
             _configurationManager.SaveConfiguration("encoding", config);
             _configurationManager.SaveConfiguration("encoding", config);
 
 
@@ -209,6 +209,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
                     _ffmpegPath = path;
                     _ffmpegPath = path;
                     EncoderLocation = location;
                     EncoderLocation = location;
+                    return true;
                 }
                 }
                 else
                 else
                 {
                 {

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

@@ -24,6 +24,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="BDInfo" Version="0.7.6.1" />
     <PackageReference Include="BDInfo" Version="0.7.6.1" />
+    <PackageReference Include="libse" Version="3.5.8" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
     <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
     <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
     <PackageReference Include="UTF.Unknown" Version="2.3.0" />
     <PackageReference Include="UTF.Unknown" Version="2.3.0" />

+ 3 - 3
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -681,9 +681,9 @@ namespace MediaBrowser.MediaEncoding.Probing
             {
             {
                 stream.Type = MediaStreamType.Subtitle;
                 stream.Type = MediaStreamType.Subtitle;
                 stream.Codec = NormalizeSubtitleCodec(stream.Codec);
                 stream.Codec = NormalizeSubtitleCodec(stream.Codec);
-                stream.localizedUndefined = _localization.GetLocalizedString("Undefined");
-                stream.localizedDefault = _localization.GetLocalizedString("Default");
-                stream.localizedForced = _localization.GetLocalizedString("Forced");
+                stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+                stream.LocalizedDefault = _localization.GetLocalizedString("Default");
+                stream.LocalizedForced = _localization.GetLocalizedString("Forced");
             }
             }
             else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
             else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
             {
             {

+ 12 - 121
MediaBrowser.MediaEncoding/Subtitles/AssParser.cs

@@ -1,130 +1,21 @@
-#pragma warning disable CS1591
+#nullable enable
 
 
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
 
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
-    public class AssParser : ISubtitleParser
+    /// <summary>
+    /// Advanced SubStation Alpha subtitle parser.
+    /// </summary>
+    public class AssParser : SubtitleEditParser<AdvancedSubStationAlpha>
     {
     {
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
-        /// <inheritdoc />
-        public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AssParser"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        public AssParser(ILogger logger) : base(logger)
         {
         {
-            var trackInfo = new SubtitleTrackInfo();
-            var trackEvents = new List<SubtitleTrackEvent>();
-            var eventIndex = 1;
-            using (var reader = new StreamReader(stream))
-            {
-                string line;
-                while (!string.Equals(reader.ReadLine(), "[Events]", StringComparison.Ordinal))
-                {
-                }
-
-                var headers = ParseFieldHeaders(reader.ReadLine());
-
-                while ((line = reader.ReadLine()) != null)
-                {
-                    cancellationToken.ThrowIfCancellationRequested();
-
-                    if (string.IsNullOrWhiteSpace(line))
-                    {
-                        continue;
-                    }
-
-                    if (line[0] == '[')
-                    {
-                        break;
-                    }
-
-                    var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) };
-                    eventIndex++;
-                    const string Dialogue = "Dialogue: ";
-                    var sections = line.Substring(Dialogue.Length).Split(',');
-
-                    subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]);
-                    subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]);
-
-                    subEvent.Text = string.Join(',', sections[headers["Text"]..]);
-                    RemoteNativeFormatting(subEvent);
-
-                    subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
-
-                    subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w0-9]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase);
-
-                    trackEvents.Add(subEvent);
-                }
-            }
-
-            trackInfo.TrackEvents = trackEvents;
-            return trackInfo;
-        }
-
-        private long GetTicks(ReadOnlySpan<char> time)
-        {
-            return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span)
-                ? span.Ticks : 0;
-        }
-
-        internal static Dictionary<string, int> ParseFieldHeaders(string line)
-        {
-            const string Format = "Format: ";
-            var fields = line.Substring(Format.Length).Split(',').Select(x => x.Trim()).ToList();
-
-            return new Dictionary<string, int>
-            {
-                { "Start", fields.IndexOf("Start") },
-                { "End", fields.IndexOf("End") },
-                { "Text", fields.IndexOf("Text") }
-            };
-        }
-
-        private void RemoteNativeFormatting(SubtitleTrackEvent p)
-        {
-            int indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal);
-            string pre = string.Empty;
-            while (indexOfBegin >= 0 && p.Text.IndexOf('}', StringComparison.Ordinal) > indexOfBegin)
-            {
-                string s = p.Text.Substring(indexOfBegin);
-                if (s.StartsWith("{\\an1}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an2}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an3}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an4}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an5}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an6}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an7}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an8}", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an9}", StringComparison.Ordinal))
-                {
-                    pre = s.Substring(0, 6);
-                }
-                else if (s.StartsWith("{\\an1\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an2\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an3\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an4\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an5\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an6\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an7\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an8\\", StringComparison.Ordinal) ||
-                    s.StartsWith("{\\an9\\", StringComparison.Ordinal))
-                {
-                    pre = s.Substring(0, 5) + "}";
-                }
-
-                int indexOfEnd = p.Text.IndexOf('}', StringComparison.Ordinal);
-                p.Text = p.Text.Remove(indexOfBegin, (indexOfEnd - indexOfBegin) + 1);
-
-                indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal);
-            }
-
-            p.Text = pre + p.Text;
         }
         }
     }
     }
 }
 }

+ 11 - 92
MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs

@@ -1,102 +1,21 @@
-#pragma warning disable CS1591
+#nullable enable
 
 
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
 
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
-    public class SrtParser : ISubtitleParser
+    /// <summary>
+    /// SubRip subtitle parser.
+    /// </summary>
+    public class SrtParser : SubtitleEditParser<SubRip>
     {
     {
-        private readonly ILogger _logger;
-
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
-        public SrtParser(ILogger logger)
-        {
-            _logger = logger;
-        }
-
-        /// <inheritdoc />
-        public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
-        {
-            var trackInfo = new SubtitleTrackInfo();
-            var trackEvents = new List<SubtitleTrackEvent>();
-            using (var reader = new StreamReader(stream))
-            {
-                string line;
-                while ((line = reader.ReadLine()) != null)
-                {
-                    cancellationToken.ThrowIfCancellationRequested();
-
-                    if (string.IsNullOrWhiteSpace(line))
-                    {
-                        continue;
-                    }
-
-                    var subEvent = new SubtitleTrackEvent { Id = line };
-                    line = reader.ReadLine();
-
-                    if (string.IsNullOrWhiteSpace(line))
-                    {
-                        continue;
-                    }
-
-                    var time = Regex.Split(line, @"[\t ]*-->[\t ]*");
-
-                    if (time.Length < 2)
-                    {
-                        // This occurs when subtitle text has an empty line as part of the text.
-                        // Need to adjust the break statement below to resolve this.
-                        _logger.LogWarning("Unrecognized line in srt: {0}", line);
-                        continue;
-                    }
-
-                    subEvent.StartPositionTicks = GetTicks(time[0]);
-                    var endTime = time[1].AsSpan();
-                    var idx = endTime.IndexOf(' ');
-                    if (idx > 0)
-                    {
-                        endTime = endTime.Slice(0, idx);
-                    }
-
-                    subEvent.EndPositionTicks = GetTicks(endTime);
-                    var multiline = new List<string>();
-                    while ((line = reader.ReadLine()) != null)
-                    {
-                        if (line.Length == 0)
-                        {
-                            break;
-                        }
-
-                        multiline.Add(line);
-                    }
-
-                    subEvent.Text = string.Join(ParserValues.NewLine, multiline);
-                    subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
-                    subEvent.Text = Regex.Replace(subEvent.Text, @"\{(?:\\[0-9]?[\w.-]+(?:\([^\)]*\)|&H?[0-9A-Fa-f]+&|))+\}", string.Empty, RegexOptions.IgnoreCase);
-                    subEvent.Text = Regex.Replace(subEvent.Text, "<", "&lt;", RegexOptions.IgnoreCase);
-                    subEvent.Text = Regex.Replace(subEvent.Text, ">", "&gt;", RegexOptions.IgnoreCase);
-                    subEvent.Text = Regex.Replace(subEvent.Text, "&lt;(\\/?(font|b|u|i|s))((\\s+(\\w|\\w[\\w\\-]*\\w)(\\s*=\\s*(?:\\\".*?\\\"|'.*?'|[^'\\\">\\s]+))?)+\\s*|\\s*)(\\/?)&gt;", "<$1$3$7>", RegexOptions.IgnoreCase);
-                    trackEvents.Add(subEvent);
-                }
-            }
-
-            trackInfo.TrackEvents = trackEvents;
-            return trackInfo;
-        }
-
-        private long GetTicks(ReadOnlySpan<char> time)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SrtParser"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        public SrtParser(ILogger logger) : base(logger)
         {
         {
-            return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span)
-                ? span.Ticks
-                : (TimeSpan.TryParseExact(time, @"hh\:mm\:ss\,fff", _usCulture, out span)
-                ? span.Ticks : 0);
         }
         }
     }
     }
 }
 }

+ 11 - 467
MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs

@@ -1,477 +1,21 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
+#nullable enable
+
+using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
 
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
     /// <summary>
     /// <summary>
-    /// <see href="https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs">Credit</see>.
+    /// SubStation Alpha subtitle parser.
     /// </summary>
     /// </summary>
-    public class SsaParser : ISubtitleParser
+    public class SsaParser : SubtitleEditParser<SubStationAlpha>
     {
     {
-        /// <inheritdoc />
-        public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SsaParser"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        public SsaParser(ILogger logger) : base(logger)
         {
         {
-            var trackInfo = new SubtitleTrackInfo();
-            var trackEvents = new List<SubtitleTrackEvent>();
-
-            using (var reader = new StreamReader(stream))
-            {
-                bool eventsStarted = false;
-
-                string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(',');
-                int indexLayer = 0;
-                int indexStart = 1;
-                int indexEnd = 2;
-                int indexStyle = 3;
-                int indexName = 4;
-                int indexEffect = 8;
-                int indexText = 9;
-                int lineNumber = 0;
-
-                var header = new StringBuilder();
-
-                string line;
-
-                while ((line = reader.ReadLine()) != null)
-                {
-                    cancellationToken.ThrowIfCancellationRequested();
-
-                    lineNumber++;
-                    if (!eventsStarted)
-                    {
-                        header.AppendLine(line);
-                    }
-
-                    if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase))
-                    {
-                        eventsStarted = true;
-                    }
-                    else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(';'))
-                    {
-                        // skip comment lines
-                    }
-                    else if (eventsStarted && line.Trim().Length > 0)
-                    {
-                        string s = line.Trim().ToLowerInvariant();
-                        if (s.StartsWith("format:", StringComparison.Ordinal))
-                        {
-                            if (line.Length > 10)
-                            {
-                                format = line.ToLowerInvariant().Substring(8).Split(',');
-                                for (int i = 0; i < format.Length; i++)
-                                {
-                                    if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        indexLayer = i;
-                                    }
-                                    else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        indexStart = i;
-                                    }
-                                    else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        indexEnd = i;
-                                    }
-                                    else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        indexText = i;
-                                    }
-                                    else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        indexEffect = i;
-                                    }
-                                    else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        indexStyle = i;
-                                    }
-                                }
-                            }
-                        }
-                        else if (!string.IsNullOrEmpty(s))
-                        {
-                            string text = string.Empty;
-                            string start = string.Empty;
-                            string end = string.Empty;
-                            string style = string.Empty;
-                            string layer = string.Empty;
-                            string effect = string.Empty;
-                            string name = string.Empty;
-
-                            string[] splittedLine;
-
-                            if (s.StartsWith("dialogue:", StringComparison.Ordinal))
-                            {
-                                splittedLine = line.Substring(10).Split(',');
-                            }
-                            else
-                            {
-                                splittedLine = line.Split(',');
-                            }
-
-                            for (int i = 0; i < splittedLine.Length; i++)
-                            {
-                                if (i == indexStart)
-                                {
-                                    start = splittedLine[i].Trim();
-                                }
-                                else if (i == indexEnd)
-                                {
-                                    end = splittedLine[i].Trim();
-                                }
-                                else if (i == indexLayer)
-                                {
-                                    layer = splittedLine[i];
-                                }
-                                else if (i == indexEffect)
-                                {
-                                    effect = splittedLine[i];
-                                }
-                                else if (i == indexText)
-                                {
-                                    text = splittedLine[i];
-                                }
-                                else if (i == indexStyle)
-                                {
-                                    style = splittedLine[i];
-                                }
-                                else if (i == indexName)
-                                {
-                                    name = splittedLine[i];
-                                }
-                                else if (i > indexText)
-                                {
-                                    text += "," + splittedLine[i];
-                                }
-                            }
-
-                            try
-                            {
-                                trackEvents.Add(
-                                    new SubtitleTrackEvent
-                                    {
-                                        StartPositionTicks = GetTimeCodeFromString(start),
-                                        EndPositionTicks = GetTimeCodeFromString(end),
-                                        Text = GetFormattedText(text)
-                                    });
-                            }
-                            catch
-                            {
-                            }
-                        }
-                    }
-                }
-
-                // if (header.Length > 0)
-                // subtitle.Header = header.ToString();
-
-                // subtitle.Renumber(1);
-            }
-
-            trackInfo.TrackEvents = trackEvents.ToArray();
-            return trackInfo;
-        }
-
-        private static long GetTimeCodeFromString(string time)
-        {
-            // h:mm:ss.cc
-            string[] timeCode = time.Split(':', '.');
-            return new TimeSpan(
-                0,
-                int.Parse(timeCode[0], CultureInfo.InvariantCulture),
-                int.Parse(timeCode[1], CultureInfo.InvariantCulture),
-                int.Parse(timeCode[2], CultureInfo.InvariantCulture),
-                int.Parse(timeCode[3], CultureInfo.InvariantCulture) * 10).Ticks;
-        }
-
-        private static string GetFormattedText(string text)
-        {
-            text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
-
-            for (int i = 0; i < 10; i++) // just look ten times...
-            {
-                if (text.Contains(@"{\fn", StringComparison.Ordinal))
-                {
-                    int start = text.IndexOf(@"{\fn", StringComparison.Ordinal);
-                    int end = text.IndexOf('}', start);
-                    if (end > 0 && !text.Substring(start).StartsWith("{\\fn}", StringComparison.Ordinal))
-                    {
-                        string fontName = text.Substring(start + 4, end - (start + 4));
-                        string extraTags = string.Empty;
-                        CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic);
-                        text = text.Remove(start, end - start + 1);
-                        if (italic)
-                        {
-                            text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + "><i>");
-                        }
-                        else
-                        {
-                            text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + ">");
-                        }
-
-                        int indexOfEndTag = text.IndexOf("{\\fn}", start, StringComparison.Ordinal);
-                        if (indexOfEndTag > 0)
-                        {
-                            text = text.Remove(indexOfEndTag, "{\\fn}".Length).Insert(indexOfEndTag, "</font>");
-                        }
-                        else
-                        {
-                            text += "</font>";
-                        }
-                    }
-                }
-
-                if (text.Contains(@"{\fs", StringComparison.Ordinal))
-                {
-                    int start = text.IndexOf(@"{\fs", StringComparison.Ordinal);
-                    int end = text.IndexOf('}', start);
-                    if (end > 0 && !text.Substring(start).StartsWith("{\\fs}", StringComparison.Ordinal))
-                    {
-                        string fontSize = text.Substring(start + 4, end - (start + 4));
-                        string extraTags = string.Empty;
-                        CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic);
-                        if (IsInteger(fontSize))
-                        {
-                            text = text.Remove(start, end - start + 1);
-                            if (italic)
-                            {
-                                text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + "><i>");
-                            }
-                            else
-                            {
-                                text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + ">");
-                            }
-
-                            int indexOfEndTag = text.IndexOf("{\\fs}", start, StringComparison.Ordinal);
-                            if (indexOfEndTag > 0)
-                            {
-                                text = text.Remove(indexOfEndTag, "{\\fs}".Length).Insert(indexOfEndTag, "</font>");
-                            }
-                            else
-                            {
-                                text += "</font>";
-                            }
-                        }
-                    }
-                }
-
-                if (text.Contains(@"{\c", StringComparison.Ordinal))
-                {
-                    int start = text.IndexOf(@"{\c", StringComparison.Ordinal);
-                    int end = text.IndexOf('}', start);
-                    if (end > 0 && !text.Substring(start).StartsWith("{\\c}", StringComparison.Ordinal))
-                    {
-                        string color = text.Substring(start + 4, end - (start + 4));
-                        string extraTags = string.Empty;
-                        CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
-
-                        color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H');
-                        color = color.PadLeft(6, '0');
-
-                        // switch to rrggbb from bbggrr
-                        color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
-                        color = color.ToLowerInvariant();
-
-                        text = text.Remove(start, end - start + 1);
-                        if (italic)
-                        {
-                            text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>");
-                        }
-                        else
-                        {
-                            text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
-                        }
-
-                        int indexOfEndTag = text.IndexOf("{\\c}", start, StringComparison.Ordinal);
-                        if (indexOfEndTag > 0)
-                        {
-                            text = text.Remove(indexOfEndTag, "{\\c}".Length).Insert(indexOfEndTag, "</font>");
-                        }
-                        else
-                        {
-                            text += "</font>";
-                        }
-                    }
-                }
-
-                if (text.Contains(@"{\1c", StringComparison.Ordinal)) // "1" specifices primary color
-                {
-                    int start = text.IndexOf(@"{\1c", StringComparison.Ordinal);
-                    int end = text.IndexOf('}', start);
-                    if (end > 0 && !text.Substring(start).StartsWith("{\\1c}", StringComparison.Ordinal))
-                    {
-                        string color = text.Substring(start + 5, end - (start + 5));
-                        string extraTags = string.Empty;
-                        CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
-
-                        color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H');
-                        color = color.PadLeft(6, '0');
-
-                        // switch to rrggbb from bbggrr
-                        color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
-                        color = color.ToLowerInvariant();
-
-                        text = text.Remove(start, end - start + 1);
-                        if (italic)
-                        {
-                            text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>");
-                        }
-                        else
-                        {
-                            text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
-                        }
-
-                        int indexOfEndTag = text.IndexOf("{\\1c}", start, StringComparison.Ordinal);
-                        if (indexOfEndTag > 0)
-                        {
-                            text = text.Remove(indexOfEndTag, "{\\1c}".Length).Insert(indexOfEndTag, "</font>");
-                        }
-                        else
-                        {
-                            text += "</font>";
-                        }
-                    }
-                }
-            }
-
-            text = text.Replace(@"{\i1}", "<i>", StringComparison.Ordinal);
-            text = text.Replace(@"{\i0}", "</i>", StringComparison.Ordinal);
-            text = text.Replace(@"{\i}", "</i>", StringComparison.Ordinal);
-            if (CountTagInText(text, "<i>") > CountTagInText(text, "</i>"))
-            {
-                text += "</i>";
-            }
-
-            text = text.Replace(@"{\u1}", "<u>", StringComparison.Ordinal);
-            text = text.Replace(@"{\u0}", "</u>", StringComparison.Ordinal);
-            text = text.Replace(@"{\u}", "</u>", StringComparison.Ordinal);
-            if (CountTagInText(text, "<u>") > CountTagInText(text, "</u>"))
-            {
-                text += "</u>";
-            }
-
-            text = text.Replace(@"{\b1}", "<b>", StringComparison.Ordinal);
-            text = text.Replace(@"{\b0}", "</b>", StringComparison.Ordinal);
-            text = text.Replace(@"{\b}", "</b>", StringComparison.Ordinal);
-            if (CountTagInText(text, "<b>") > CountTagInText(text, "</b>"))
-            {
-                text += "</b>";
-            }
-
-            return text;
-        }
-
-        private static bool IsInteger(string s)
-            => int.TryParse(s, out _);
-
-        private static int CountTagInText(string text, string tag)
-        {
-            int count = 0;
-            int index = text.IndexOf(tag, StringComparison.Ordinal);
-            while (index >= 0)
-            {
-                count++;
-                if (index == text.Length)
-                {
-                    return count;
-                }
-
-                index = text.IndexOf(tag, index + 1, StringComparison.Ordinal);
-            }
-
-            return count;
-        }
-
-        private static void CheckAndAddSubTags(ref string tagName, ref string extraTags, out bool italic)
-        {
-            italic = false;
-            int indexOfSPlit = tagName.IndexOf('\\', StringComparison.Ordinal);
-            if (indexOfSPlit > 0)
-            {
-                string rest = tagName.Substring(indexOfSPlit).TrimStart('\\');
-                tagName = tagName.Remove(indexOfSPlit);
-
-                for (int i = 0; i < 10; i++)
-                {
-                    if (rest.StartsWith("fs", StringComparison.Ordinal) && rest.Length > 2)
-                    {
-                        indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
-                        string fontSize = rest;
-                        if (indexOfSPlit > 0)
-                        {
-                            fontSize = rest.Substring(0, indexOfSPlit);
-                            rest = rest.Substring(indexOfSPlit).TrimStart('\\');
-                        }
-                        else
-                        {
-                            rest = string.Empty;
-                        }
-
-                        extraTags += " size=\"" + fontSize.Substring(2) + "\"";
-                    }
-                    else if (rest.StartsWith("fn", StringComparison.Ordinal) && rest.Length > 2)
-                    {
-                        indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
-                        string fontName = rest;
-                        if (indexOfSPlit > 0)
-                        {
-                            fontName = rest.Substring(0, indexOfSPlit);
-                            rest = rest.Substring(indexOfSPlit).TrimStart('\\');
-                        }
-                        else
-                        {
-                            rest = string.Empty;
-                        }
-
-                        extraTags += " face=\"" + fontName.Substring(2) + "\"";
-                    }
-                    else if (rest.StartsWith("c", StringComparison.Ordinal) && rest.Length > 2)
-                    {
-                        indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
-                        string fontColor = rest;
-                        if (indexOfSPlit > 0)
-                        {
-                            fontColor = rest.Substring(0, indexOfSPlit);
-                            rest = rest.Substring(indexOfSPlit).TrimStart('\\');
-                        }
-                        else
-                        {
-                            rest = string.Empty;
-                        }
-
-                        string color = fontColor.Substring(2);
-                        color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H');
-                        color = color.PadLeft(6, '0');
-                        // switch to rrggbb from bbggrr
-                        color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
-                        color = color.ToLowerInvariant();
-
-                        extraTags += " color=\"" + color + "\"";
-                    }
-                    else if (rest.StartsWith("i1", StringComparison.Ordinal) && rest.Length > 1)
-                    {
-                        indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
-                        italic = true;
-                        if (indexOfSPlit > 0)
-                        {
-                            rest = rest.Substring(indexOfSPlit).TrimStart('\\');
-                        }
-                        else
-                        {
-                            rest = string.Empty;
-                        }
-                    }
-                    else if (rest.Length > 0 && rest.Contains('\\', StringComparison.Ordinal))
-                    {
-                        indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
-                        rest = rest.Substring(indexOfSPlit).TrimStart('\\');
-                    }
-                }
-            }
         }
         }
     }
     }
 }
 }

+ 63 - 0
MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs

@@ -0,0 +1,63 @@
+#nullable enable
+
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
+
+namespace MediaBrowser.MediaEncoding.Subtitles
+{
+    /// <summary>
+    /// SubStation Alpha subtitle parser.
+    /// </summary>
+    /// <typeparam name="T">The <see cref="SubtitleFormat" />.</typeparam>
+    public abstract class SubtitleEditParser<T> : ISubtitleParser
+        where T : SubtitleFormat, new()
+    {
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SubtitleEditParser{T}"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        protected SubtitleEditParser(ILogger logger)
+        {
+            _logger = logger;
+        }
+
+        /// <inheritdoc />
+        public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
+        {
+            var subtitle = new Subtitle();
+            var subRip = new T();
+            var lines = stream.ReadAllLines().ToList();
+            subRip.LoadSubtitle(subtitle, lines, "untitled");
+            if (subRip.ErrorCount > 0)
+            {
+                _logger.LogError("{ErrorCount} errors encountered while parsing subtitle.");
+            }
+
+            var trackInfo = new SubtitleTrackInfo();
+            int len = subtitle.Paragraphs.Count;
+            var trackEvents = new SubtitleTrackEvent[len];
+            for (int i = 0; i < len; i++)
+            {
+                var p = subtitle.Paragraphs[i];
+                trackEvents[i] = new SubtitleTrackEvent(p.Number.ToString(CultureInfo.InvariantCulture), p.Text)
+                {
+                    StartPositionTicks = p.StartTime.TimeSpan.Ticks,
+                    EndPositionTicks = p.EndTime.TimeSpan.Ticks
+                };
+            }
+
+            trackInfo.TrackEvents = trackEvents;
+            return trackInfo;
+        }
+    }
+}

+ 22 - 32
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -27,7 +27,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
     public class SubtitleEncoder : ISubtitleEncoder
     public class SubtitleEncoder : ISubtitleEncoder
     {
     {
-        private readonly ILibraryManager _libraryManager;
         private readonly ILogger<SubtitleEncoder> _logger;
         private readonly ILogger<SubtitleEncoder> _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
@@ -42,7 +41,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             new ConcurrentDictionary<string, SemaphoreSlim>();
             new ConcurrentDictionary<string, SemaphoreSlim>();
 
 
         public SubtitleEncoder(
         public SubtitleEncoder(
-            ILibraryManager libraryManager,
             ILogger<SubtitleEncoder> logger,
             ILogger<SubtitleEncoder> logger,
             IApplicationPaths appPaths,
             IApplicationPaths appPaths,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
@@ -50,7 +48,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             IHttpClientFactory httpClientFactory,
             IHttpClientFactory httpClientFactory,
             IMediaSourceManager mediaSourceManager)
             IMediaSourceManager mediaSourceManager)
         {
         {
-            _libraryManager = libraryManager;
             _logger = logger;
             _logger = logger;
             _appPaths = appPaths;
             _appPaths = appPaths;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -168,33 +165,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             MediaStream subtitleStream,
             MediaStream subtitleStream,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
-            var inputFile = mediaSource.Path;
+            var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
 
 
-            var protocol = mediaSource.Protocol;
-            if (subtitleStream.IsExternal)
-            {
-                protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
-            }
-
-            var fileInfo = await GetReadableFile(mediaSource.Path, inputFile, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
-
-            var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
+            var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
 
 
             return (stream, fileInfo.Format);
             return (stream, fileInfo.Format);
         }
         }
 
 
-        private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
+        private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
         {
         {
-            if (requiresCharset)
+            if (fileInfo.IsExternal)
             {
             {
-                using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
+                using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false))
                 {
                 {
                     var result = CharsetDetector.DetectFromStream(stream).Detected;
                     var result = CharsetDetector.DetectFromStream(stream).Detected;
                     stream.Position = 0;
                     stream.Position = 0;
 
 
                     if (result != null)
                     if (result != null)
                     {
                     {
-                        _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path);
+                        _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, fileInfo.Path);
 
 
                         using var reader = new StreamReader(stream, result.Encoding);
                         using var reader = new StreamReader(stream, result.Encoding);
                         var text = await reader.ReadToEndAsync().ConfigureAwait(false);
                         var text = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -204,12 +193,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
                 }
             }
             }
 
 
-            return File.OpenRead(path);
+            return File.OpenRead(fileInfo.Path);
         }
         }
 
 
         private async Task<SubtitleInfo> GetReadableFile(
         private async Task<SubtitleInfo> GetReadableFile(
-            string mediaPath,
-            string inputFile,
             MediaSourceInfo mediaSource,
             MediaSourceInfo mediaSource,
             MediaStream subtitleStream,
             MediaStream subtitleStream,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
@@ -241,9 +228,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
                 }
 
 
                 // Extract
                 // Extract
-                var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat);
+                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
 
 
-                await ExtractTextSubtitle(inputFile, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
+                await ExtractTextSubtitle(mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
                         .ConfigureAwait(false);
                         .ConfigureAwait(false);
 
 
                 return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
                 return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
@@ -255,13 +242,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             if (GetReader(currentFormat, false) == null)
             if (GetReader(currentFormat, false) == null)
             {
             {
                 // Convert
                 // Convert
-                var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, ".srt");
+                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
 
 
                 await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
                 await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
 
 
                 return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
                 return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
             }
             }
 
 
+            if (subtitleStream.IsExternal)
+            {
+                return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
+            }
+
             return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
             return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
         }
         }
 
 
@@ -279,12 +271,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
             if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
             if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
             {
             {
-                return new SsaParser();
+                return new SsaParser(_logger);
             }
             }
 
 
             if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
             if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
             {
             {
-                return new AssParser();
+                return new AssParser(_logger);
             }
             }
 
 
             if (throwIfMissing)
             if (throwIfMissing)
@@ -504,7 +496,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// <summary>
         /// <summary>
         /// Extracts the text subtitle.
         /// Extracts the text subtitle.
         /// </summary>
         /// </summary>
-        /// <param name="inputFile">The input file.</param>
         /// <param name="mediaSource">The mediaSource.</param>
         /// <param name="mediaSource">The mediaSource.</param>
         /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
         /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
         /// <param name="outputCodec">The output codec.</param>
         /// <param name="outputCodec">The output codec.</param>
@@ -513,7 +504,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
         /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
         private async Task ExtractTextSubtitle(
         private async Task ExtractTextSubtitle(
-            string inputFile,
             MediaSourceInfo mediaSource,
             MediaSourceInfo mediaSource,
             int subtitleStreamIndex,
             int subtitleStreamIndex,
             string outputCodec,
             string outputCodec,
@@ -529,7 +519,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 if (!File.Exists(outputPath))
                 if (!File.Exists(outputPath))
                 {
                 {
                     await ExtractTextSubtitleInternal(
                     await ExtractTextSubtitleInternal(
-                        _mediaEncoder.GetInputArgument(inputFile, mediaSource),
+                        _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource),
                         subtitleStreamIndex,
                         subtitleStreamIndex,
                         outputCodec,
                         outputCodec,
                         outputPath,
                         outputPath,
@@ -695,15 +685,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
             }
         }
         }
 
 
-        private string GetSubtitleCachePath(string mediaPath, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
+        private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
         {
         {
             if (mediaSource.Protocol == MediaProtocol.File)
             if (mediaSource.Protocol == MediaProtocol.File)
             {
             {
                 var ticksParam = string.Empty;
                 var ticksParam = string.Empty;
 
 
-                var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
+                var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path);
 
 
-                ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
+                ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
 
 
                 var prefix = filename.Slice(0, 1);
                 var prefix = filename.Slice(0, 1);
 
 
@@ -711,7 +701,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
             }
             else
             else
             {
             {
-                ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
+                ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
 
 
                 var prefix = filename.Slice(0, 1);
                 var prefix = filename.Slice(0, 1);
 
 

+ 9 - 9
MediaBrowser.Model/Channels/ChannelFeatures.cs

@@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Channels
 {
 {
     public class ChannelFeatures
     public class ChannelFeatures
     {
     {
+        public ChannelFeatures()
+        {
+            MediaTypes = Array.Empty<ChannelMediaType>();
+            ContentTypes = Array.Empty<ChannelMediaContentType>();
+            DefaultSortFields = Array.Empty<ChannelItemSortField>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>
@@ -38,7 +45,7 @@ namespace MediaBrowser.Model.Channels
         public ChannelMediaContentType[] ContentTypes { get; set; }
         public ChannelMediaContentType[] ContentTypes { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Represents the maximum number of records the channel allows retrieving at a time.
+        /// Gets or sets the maximum number of records the channel allows retrieving at a time.
         /// </summary>
         /// </summary>
         public int? MaxPageSize { get; set; }
         public int? MaxPageSize { get; set; }
 
 
@@ -55,7 +62,7 @@ namespace MediaBrowser.Model.Channels
         public ChannelItemSortField[] DefaultSortFields { get; set; }
         public ChannelItemSortField[] DefaultSortFields { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Indicates if a sort ascending/descending toggle is supported or not.
+        /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported.
         /// </summary>
         /// </summary>
         public bool SupportsSortOrderToggle { get; set; }
         public bool SupportsSortOrderToggle { get; set; }
 
 
@@ -76,12 +83,5 @@ namespace MediaBrowser.Model.Channels
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if [supports content downloading]; otherwise, <c>false</c>.</value>
         /// <value><c>true</c> if [supports content downloading]; otherwise, <c>false</c>.</value>
         public bool SupportsContentDownloading { get; set; }
         public bool SupportsContentDownloading { get; set; }
-
-        public ChannelFeatures()
-        {
-            MediaTypes = Array.Empty<ChannelMediaType>();
-            ContentTypes = Array.Empty<ChannelMediaContentType>();
-            DefaultSortFields = Array.Empty<ChannelItemSortField>();
-        }
     }
     }
 }
 }

+ 3 - 3
MediaBrowser.Model/Channels/ChannelQuery.cs

@@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Channels
     public class ChannelQuery
     public class ChannelQuery
     {
     {
         /// <summary>
         /// <summary>
-        /// Fields to return within the items, in addition to basic information.
+        /// Gets or sets the fields to return within the items, in addition to basic information.
         /// </summary>
         /// </summary>
         /// <value>The fields.</value>
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }
         public ItemFields[] Fields { get; set; }
@@ -28,13 +28,13 @@ namespace MediaBrowser.Model.Channels
         public Guid UserId { get; set; }
         public Guid UserId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Use for paging.
         /// </summary>
         /// </summary>
         /// <value>The start index.</value>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
         public int? StartIndex { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// </summary>
         /// <value>The limit.</value>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
         public int? Limit { get; set; }

+ 37 - 37
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -5,6 +5,41 @@ namespace MediaBrowser.Model.Configuration
 {
 {
     public class EncodingOptions
     public class EncodingOptions
     {
     {
+        public EncodingOptions()
+        {
+            EnableFallbackFont = false;
+            DownMixAudioBoost = 2;
+            MaxMuxingQueueSize = 2048;
+            EnableThrottling = false;
+            ThrottleDelaySeconds = 180;
+            EncodingThreadCount = -1;
+            // This is a DRM device that is almost guaranteed to be there on every intel platform,
+            // plus it's the default one in ffmpeg if you don't specify anything
+            VaapiDevice = "/dev/dri/renderD128";
+            // This is the OpenCL device that is used for tonemapping.
+            // The left side of the dot is the platform number, and the right side is the device number on the platform.
+            OpenclDevice = "0.0";
+            EnableTonemapping = false;
+            EnableVppTonemapping = false;
+            TonemappingAlgorithm = "hable";
+            TonemappingRange = "auto";
+            TonemappingDesat = 0;
+            TonemappingThreshold = 0.8;
+            TonemappingPeak = 100;
+            TonemappingParam = 0;
+            H264Crf = 23;
+            H265Crf = 28;
+            DeinterlaceDoubleRate = false;
+            DeinterlaceMethod = "yadif";
+            EnableDecodingColorDepth10Hevc = true;
+            EnableDecodingColorDepth10Vp9 = true;
+            EnableEnhancedNvdecDecoder = true;
+            EnableHardwareEncoding = true;
+            AllowHevcEncoding = true;
+            EnableSubtitleExtraction = true;
+            HardwareDecodingCodecs = new string[] { "h264", "vc1" };
+        }
+
         public int EncodingThreadCount { get; set; }
         public int EncodingThreadCount { get; set; }
 
 
         public string TranscodingTempPath { get; set; }
         public string TranscodingTempPath { get; set; }
@@ -24,12 +59,12 @@ namespace MediaBrowser.Model.Configuration
         public string HardwareAccelerationType { get; set; }
         public string HardwareAccelerationType { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// FFmpeg path as set by the user via the UI.
+        /// Gets or sets the FFmpeg path as set by the user via the UI.
         /// </summary>
         /// </summary>
         public string EncoderAppPath { get; set; }
         public string EncoderAppPath { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The current FFmpeg path being used by the system and displayed on the transcode page.
+        /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page.
         /// </summary>
         /// </summary>
         public string EncoderAppPathDisplay { get; set; }
         public string EncoderAppPathDisplay { get; set; }
 
 
@@ -76,40 +111,5 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableSubtitleExtraction { get; set; }
         public bool EnableSubtitleExtraction { get; set; }
 
 
         public string[] HardwareDecodingCodecs { get; set; }
         public string[] HardwareDecodingCodecs { get; set; }
-
-        public EncodingOptions()
-        {
-            EnableFallbackFont = false;
-            DownMixAudioBoost = 2;
-            MaxMuxingQueueSize = 2048;
-            EnableThrottling = false;
-            ThrottleDelaySeconds = 180;
-            EncodingThreadCount = -1;
-            // This is a DRM device that is almost guaranteed to be there on every intel platform,
-            // plus it's the default one in ffmpeg if you don't specify anything
-            VaapiDevice = "/dev/dri/renderD128";
-            // This is the OpenCL device that is used for tonemapping.
-            // The left side of the dot is the platform number, and the right side is the device number on the platform.
-            OpenclDevice = "0.0";
-            EnableTonemapping = false;
-            EnableVppTonemapping = false;
-            TonemappingAlgorithm = "hable";
-            TonemappingRange = "auto";
-            TonemappingDesat = 0;
-            TonemappingThreshold = 0.8;
-            TonemappingPeak = 100;
-            TonemappingParam = 0;
-            H264Crf = 23;
-            H265Crf = 28;
-            DeinterlaceDoubleRate = false;
-            DeinterlaceMethod = "yadif";
-            EnableDecodingColorDepth10Hevc = true;
-            EnableDecodingColorDepth10Vp9 = true;
-            EnableEnhancedNvdecDecoder = true;
-            EnableHardwareEncoding = true;
-            AllowHevcEncoding = true;
-            EnableSubtitleExtraction = true;
-            HardwareDecodingCodecs = new string[] { "h264", "vc1" };
-        }
     }
     }
 }
 }

+ 5 - 5
MediaBrowser.Model/Configuration/ImageOption.cs

@@ -6,6 +6,11 @@ namespace MediaBrowser.Model.Configuration
 {
 {
     public class ImageOption
     public class ImageOption
     {
     {
+        public ImageOption()
+        {
+            Limit = 1;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the type.
         /// Gets or sets the type.
         /// </summary>
         /// </summary>
@@ -23,10 +28,5 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// </summary>
         /// <value>The minimum width.</value>
         /// <value>The minimum width.</value>
         public int MinWidth { get; set; }
         public int MinWidth { get; set; }
-
-        public ImageOption()
-        {
-            Limit = 1;
-        }
     }
     }
 }
 }

+ 19 - 384
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -2,13 +2,30 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Model.Configuration
 namespace MediaBrowser.Model.Configuration
 {
 {
     public class LibraryOptions
     public class LibraryOptions
     {
     {
+        public LibraryOptions()
+        {
+            TypeOptions = Array.Empty<TypeOptions>();
+            DisabledSubtitleFetchers = Array.Empty<string>();
+            SubtitleFetcherOrder = Array.Empty<string>();
+            DisabledLocalMetadataReaders = Array.Empty<string>();
+
+            SkipSubtitlesIfAudioTrackMatches = true;
+            RequirePerfectSubtitleMatch = true;
+
+            EnablePhotos = true;
+            SaveSubtitlesWithMedia = true;
+            EnableRealtimeMonitor = true;
+            PathInfos = Array.Empty<MediaPathInfo>();
+            EnableInternetProviders = true;
+            EnableAutomaticSeriesGrouping = true;
+            SeasonZeroDisplayName = "Specials";
+        }
+
         public bool EnablePhotos { get; set; }
         public bool EnablePhotos { get; set; }
 
 
         public bool EnableRealtimeMonitor { get; set; }
         public bool EnableRealtimeMonitor { get; set; }
@@ -79,387 +96,5 @@ namespace MediaBrowser.Model.Configuration
 
 
             return null;
             return null;
         }
         }
-
-        public LibraryOptions()
-        {
-            TypeOptions = Array.Empty<TypeOptions>();
-            DisabledSubtitleFetchers = Array.Empty<string>();
-            SubtitleFetcherOrder = Array.Empty<string>();
-            DisabledLocalMetadataReaders = Array.Empty<string>();
-
-            SkipSubtitlesIfAudioTrackMatches = true;
-            RequirePerfectSubtitleMatch = true;
-
-            EnablePhotos = true;
-            SaveSubtitlesWithMedia = true;
-            EnableRealtimeMonitor = true;
-            PathInfos = Array.Empty<MediaPathInfo>();
-            EnableInternetProviders = true;
-            EnableAutomaticSeriesGrouping = true;
-            SeasonZeroDisplayName = "Specials";
-        }
-    }
-
-    public class MediaPathInfo
-    {
-        public string Path { get; set; }
-
-        public string NetworkPath { get; set; }
-    }
-
-    public class TypeOptions
-    {
-        public string Type { get; set; }
-
-        public string[] MetadataFetchers { get; set; }
-
-        public string[] MetadataFetcherOrder { get; set; }
-
-        public string[] ImageFetchers { get; set; }
-
-        public string[] ImageFetcherOrder { get; set; }
-
-        public ImageOption[] ImageOptions { get; set; }
-
-        public ImageOption GetImageOptions(ImageType type)
-        {
-            foreach (var i in ImageOptions)
-            {
-                if (i.Type == type)
-                {
-                    return i;
-                }
-            }
-
-            if (DefaultImageOptions.TryGetValue(Type, out ImageOption[] options))
-            {
-                foreach (var i in options)
-                {
-                    if (i.Type == type)
-                    {
-                        return i;
-                    }
-                }
-            }
-
-            return DefaultInstance;
-        }
-
-        public int GetLimit(ImageType type)
-        {
-            return GetImageOptions(type).Limit;
-        }
-
-        public int GetMinWidth(ImageType type)
-        {
-            return GetImageOptions(type).MinWidth;
-        }
-
-        public bool IsEnabled(ImageType type)
-        {
-            return GetLimit(type) > 0;
-        }
-
-        public TypeOptions()
-        {
-            MetadataFetchers = Array.Empty<string>();
-            MetadataFetcherOrder = Array.Empty<string>();
-            ImageFetchers = Array.Empty<string>();
-            ImageFetcherOrder = Array.Empty<string>();
-            ImageOptions = Array.Empty<ImageOption>();
-        }
-
-        public static Dictionary<string, ImageOption[]> DefaultImageOptions = new Dictionary<string, ImageOption[]>
-        {
-            {
-                "Movie", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Art
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Disc
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Primary
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Banner
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Thumb
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Logo
-                    }
-                }
-            },
-            {
-                "MusicVideo", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Art
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Disc
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Primary
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Banner
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Thumb
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Logo
-                    }
-                }
-            },
-            {
-                "Series", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Art
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Primary
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Banner
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Thumb
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Logo
-                    }
-                }
-            },
-            {
-                "MusicAlbum", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Disc
-                    }
-                }
-            },
-            {
-                "MusicArtist", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    // Don't download this by default
-                    // They do look great, but most artists won't have them, which means a banner view isn't really possible
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Banner
-                    },
-
-                    // Don't download this by default
-                    // Generally not used
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Art
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Logo
-                    }
-                }
-            },
-            {
-                "BoxSet", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Primary
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Thumb
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Logo
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Art
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Disc
-                    },
-
-                    // Don't download this by default as it's rarely used.
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Banner
-                    }
-                }
-            },
-            {
-                "Season", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Primary
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Banner
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        Type = ImageType.Thumb
-                    }
-                }
-            },
-            {
-                "Episode", new []
-                {
-                    new ImageOption
-                    {
-                        Limit = 0,
-                        MinWidth = 1280,
-                        Type = ImageType.Backdrop
-                    },
-
-                    new ImageOption
-                    {
-                        Limit = 1,
-                        Type = ImageType.Primary
-                    }
-                }
-            }
-        };
-
-        public static ImageOption DefaultInstance = new ImageOption();
     }
     }
 }
 }

+ 12 - 0
MediaBrowser.Model/Configuration/MediaPathInfo.cs

@@ -0,0 +1,12 @@
+#nullable disable
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Configuration
+{
+    public class MediaPathInfo
+    {
+        public string Path { get; set; }
+
+        public string NetworkPath { get; set; }
+    }
+}

+ 2 - 2
MediaBrowser.Model/Configuration/MetadataConfiguration.cs

@@ -4,11 +4,11 @@ namespace MediaBrowser.Model.Configuration
 {
 {
     public class MetadataConfiguration
     public class MetadataConfiguration
     {
     {
-        public bool UseFileCreationTimeForDateAdded { get; set; }
-
         public MetadataConfiguration()
         public MetadataConfiguration()
         {
         {
             UseFileCreationTimeForDateAdded = true;
             UseFileCreationTimeForDateAdded = true;
         }
         }
+
+        public bool UseFileCreationTimeForDateAdded { get; set; }
     }
     }
 }
 }

+ 10 - 10
MediaBrowser.Model/Configuration/MetadataOptions.cs

@@ -10,6 +10,16 @@ namespace MediaBrowser.Model.Configuration
     /// </summary>
     /// </summary>
     public class MetadataOptions
     public class MetadataOptions
     {
     {
+        public MetadataOptions()
+        {
+            DisabledMetadataSavers = Array.Empty<string>();
+            LocalMetadataReaderOrder = Array.Empty<string>();
+            DisabledMetadataFetchers = Array.Empty<string>();
+            MetadataFetcherOrder = Array.Empty<string>();
+            DisabledImageFetchers = Array.Empty<string>();
+            ImageFetcherOrder = Array.Empty<string>();
+        }
+
         public string ItemType { get; set; }
         public string ItemType { get; set; }
 
 
         public string[] DisabledMetadataSavers { get; set; }
         public string[] DisabledMetadataSavers { get; set; }
@@ -23,15 +33,5 @@ namespace MediaBrowser.Model.Configuration
         public string[] DisabledImageFetchers { get; set; }
         public string[] DisabledImageFetchers { get; set; }
 
 
         public string[] ImageFetcherOrder { get; set; }
         public string[] ImageFetcherOrder { get; set; }
-
-        public MetadataOptions()
-        {
-            DisabledMetadataSavers = Array.Empty<string>();
-            LocalMetadataReaderOrder = Array.Empty<string>();
-            DisabledMetadataFetchers = Array.Empty<string>();
-            MetadataFetcherOrder = Array.Empty<string>();
-            DisabledImageFetchers = Array.Empty<string>();
-            ImageFetcherOrder = Array.Empty<string>();
-        }
     }
     }
 }
 }

+ 6 - 6
MediaBrowser.Model/Configuration/MetadataPluginSummary.cs

@@ -8,6 +8,12 @@ namespace MediaBrowser.Model.Configuration
 {
 {
     public class MetadataPluginSummary
     public class MetadataPluginSummary
     {
     {
+        public MetadataPluginSummary()
+        {
+            SupportedImageTypes = Array.Empty<ImageType>();
+            Plugins = Array.Empty<MetadataPlugin>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the type of the item.
         /// Gets or sets the type of the item.
         /// </summary>
         /// </summary>
@@ -25,11 +31,5 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// </summary>
         /// <value>The supported image types.</value>
         /// <value>The supported image types.</value>
         public ImageType[] SupportedImageTypes { get; set; }
         public ImageType[] SupportedImageTypes { get; set; }
-
-        public MetadataPluginSummary()
-        {
-            SupportedImageTypes = Array.Empty<ImageType>();
-            Plugins = Array.Empty<MetadataPlugin>();
-        }
     }
     }
 }
 }

+ 365 - 0
MediaBrowser.Model/Configuration/TypeOptions.cs

@@ -0,0 +1,365 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Configuration
+{
+    public class TypeOptions
+    {
+        public static readonly ImageOption DefaultInstance = new ImageOption();
+
+        public static readonly Dictionary<string, ImageOption[]> DefaultImageOptions = new Dictionary<string, ImageOption[]>
+        {
+            {
+                "Movie", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Art
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Disc
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Primary
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Banner
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Thumb
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Logo
+                    }
+                }
+            },
+            {
+                "MusicVideo", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Art
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Disc
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Primary
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Banner
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Thumb
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Logo
+                    }
+                }
+            },
+            {
+                "Series", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Art
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Primary
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Banner
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Thumb
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Logo
+                    }
+                }
+            },
+            {
+                "MusicAlbum", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Disc
+                    }
+                }
+            },
+            {
+                "MusicArtist", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    // Don't download this by default
+                    // They do look great, but most artists won't have them, which means a banner view isn't really possible
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Banner
+                    },
+
+                    // Don't download this by default
+                    // Generally not used
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Art
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Logo
+                    }
+                }
+            },
+            {
+                "BoxSet", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Primary
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Thumb
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Logo
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Art
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Disc
+                    },
+
+                    // Don't download this by default as it's rarely used.
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Banner
+                    }
+                }
+            },
+            {
+                "Season", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Primary
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Banner
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        Type = ImageType.Thumb
+                    }
+                }
+            },
+            {
+                "Episode", new[]
+                {
+                    new ImageOption
+                    {
+                        Limit = 0,
+                        MinWidth = 1280,
+                        Type = ImageType.Backdrop
+                    },
+
+                    new ImageOption
+                    {
+                        Limit = 1,
+                        Type = ImageType.Primary
+                    }
+                }
+            }
+        };
+
+        public TypeOptions()
+        {
+            MetadataFetchers = Array.Empty<string>();
+            MetadataFetcherOrder = Array.Empty<string>();
+            ImageFetchers = Array.Empty<string>();
+            ImageFetcherOrder = Array.Empty<string>();
+            ImageOptions = Array.Empty<ImageOption>();
+        }
+
+        public string Type { get; set; }
+
+        public string[] MetadataFetchers { get; set; }
+
+        public string[] MetadataFetcherOrder { get; set; }
+
+        public string[] ImageFetchers { get; set; }
+
+        public string[] ImageFetcherOrder { get; set; }
+
+        public ImageOption[] ImageOptions { get; set; }
+
+        public ImageOption GetImageOptions(ImageType type)
+        {
+            foreach (var i in ImageOptions)
+            {
+                if (i.Type == type)
+                {
+                    return i;
+                }
+            }
+
+            if (DefaultImageOptions.TryGetValue(Type, out ImageOption[] options))
+            {
+                foreach (var i in options)
+                {
+                    if (i.Type == type)
+                    {
+                        return i;
+                    }
+                }
+            }
+
+            return DefaultInstance;
+        }
+
+        public int GetLimit(ImageType type)
+        {
+            return GetImageOptions(type).Limit;
+        }
+
+        public int GetMinWidth(ImageType type)
+        {
+            return GetImageOptions(type).MinWidth;
+        }
+
+        public bool IsEnabled(ImageType type)
+        {
+            return GetLimit(type) > 0;
+        }
+    }
+}

+ 18 - 18
MediaBrowser.Model/Configuration/UserConfiguration.cs

@@ -11,6 +11,24 @@ namespace MediaBrowser.Model.Configuration
     /// </summary>
     /// </summary>
     public class UserConfiguration
     public class UserConfiguration
     {
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
+        /// </summary>
+        public UserConfiguration()
+        {
+            EnableNextEpisodeAutoPlay = true;
+            RememberAudioSelections = true;
+            RememberSubtitleSelections = true;
+
+            HidePlayedInLatest = true;
+            PlayDefaultAudioTrack = true;
+
+            LatestItemsExcludes = Array.Empty<string>();
+            OrderedViews = Array.Empty<string>();
+            MyMediaExcludes = Array.Empty<string>();
+            GroupedFolders = Array.Empty<string>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the audio language preference.
         /// Gets or sets the audio language preference.
         /// </summary>
         /// </summary>
@@ -52,23 +70,5 @@ namespace MediaBrowser.Model.Configuration
         public bool RememberSubtitleSelections { get; set; }
         public bool RememberSubtitleSelections { get; set; }
 
 
         public bool EnableNextEpisodeAutoPlay { get; set; }
         public bool EnableNextEpisodeAutoPlay { get; set; }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
-        /// </summary>
-        public UserConfiguration()
-        {
-            EnableNextEpisodeAutoPlay = true;
-            RememberAudioSelections = true;
-            RememberSubtitleSelections = true;
-
-            HidePlayedInLatest = true;
-            PlayDefaultAudioTrack = true;
-
-            LatestItemsExcludes = Array.Empty<string>();
-            OrderedViews = Array.Empty<string>();
-            MyMediaExcludes = Array.Empty<string>();
-            GroupedFolders = Array.Empty<string>();
-        }
     }
     }
 }
 }

+ 8 - 8
MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs

@@ -5,6 +5,14 @@ namespace MediaBrowser.Model.Configuration
 {
 {
     public class XbmcMetadataOptions
     public class XbmcMetadataOptions
     {
     {
+        public XbmcMetadataOptions()
+        {
+            ReleaseDateFormat = "yyyy-MM-dd";
+
+            SaveImagePathsInNfo = true;
+            EnablePathSubstitution = true;
+        }
+
         public string UserId { get; set; }
         public string UserId { get; set; }
 
 
         public string ReleaseDateFormat { get; set; }
         public string ReleaseDateFormat { get; set; }
@@ -14,13 +22,5 @@ namespace MediaBrowser.Model.Configuration
         public bool EnablePathSubstitution { get; set; }
         public bool EnablePathSubstitution { get; set; }
 
 
         public bool EnableExtraThumbsDuplication { get; set; }
         public bool EnableExtraThumbsDuplication { get; set; }
-
-        public XbmcMetadataOptions()
-        {
-            ReleaseDateFormat = "yyyy-MM-dd";
-
-            SaveImagePathsInNfo = true;
-            EnablePathSubstitution = true;
-        }
     }
     }
 }
 }

+ 5 - 4
MediaBrowser.Model/Dlna/AudioOptions.cs

@@ -34,20 +34,20 @@ namespace MediaBrowser.Model.Dlna
         public DeviceProfile Profile { get; set; }
         public DeviceProfile Profile { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
+        /// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
         /// </summary>
         /// </summary>
         public string MediaSourceId { get; set; }
         public string MediaSourceId { get; set; }
 
 
         public string DeviceId { get; set; }
         public string DeviceId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Allows an override of supported number of audio channels
-        /// Example: DeviceProfile supports five channel, but user only has stereo speakers
+        /// Gets or sets an override of supported number of audio channels
+        /// Example: DeviceProfile supports five channel, but user only has stereo speakers.
         /// </summary>
         /// </summary>
         public int? MaxAudioChannels { get; set; }
         public int? MaxAudioChannels { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The application's configured quality setting.
+        /// Gets or sets the application's configured quality setting.
         /// </summary>
         /// </summary>
         public int? MaxBitrate { get; set; }
         public int? MaxBitrate { get; set; }
 
 
@@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Dlna
         /// <summary>
         /// <summary>
         /// Gets the maximum bitrate.
         /// Gets the maximum bitrate.
         /// </summary>
         /// </summary>
+        /// <param name="isAudio">Whether or not this is audio.</param>
         /// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
         /// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
         public int? GetMaxBitrate(bool isAudio)
         public int? GetMaxBitrate(bool isAudio)
         {
         {

+ 6 - 6
MediaBrowser.Model/Dlna/CodecProfile.cs

@@ -9,6 +9,12 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class CodecProfile
     public class CodecProfile
     {
     {
+        public CodecProfile()
+        {
+            Conditions = Array.Empty<ProfileCondition>();
+            ApplyConditions = Array.Empty<ProfileCondition>();
+        }
+
         [XmlAttribute("type")]
         [XmlAttribute("type")]
         public CodecType Type { get; set; }
         public CodecType Type { get; set; }
 
 
@@ -22,12 +28,6 @@ namespace MediaBrowser.Model.Dlna
         [XmlAttribute("container")]
         [XmlAttribute("container")]
         public string Container { get; set; }
         public string Container { get; set; }
 
 
-        public CodecProfile()
-        {
-            Conditions = Array.Empty<ProfileCondition>();
-            ApplyConditions = Array.Empty<ProfileCondition>();
-        }
-
         public string[] GetCodecs()
         public string[] GetCodecs()
         {
         {
             return ContainerProfile.SplitValue(Codec);
             return ContainerProfile.SplitValue(Codec);

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

@@ -1,8 +1,8 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using System.Linq;
 using System.Globalization;
 using System.Globalization;
+using System.Linq;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 
 
 namespace MediaBrowser.Model.Dlna
 namespace MediaBrowser.Model.Dlna

+ 5 - 5
MediaBrowser.Model/Dlna/ContainerProfile.cs

@@ -9,6 +9,11 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class ContainerProfile
     public class ContainerProfile
     {
     {
+        public ContainerProfile()
+        {
+            Conditions = Array.Empty<ProfileCondition>();
+        }
+
         [XmlAttribute("type")]
         [XmlAttribute("type")]
         public DlnaProfileType Type { get; set; }
         public DlnaProfileType Type { get; set; }
 
 
@@ -17,11 +22,6 @@ namespace MediaBrowser.Model.Dlna
         [XmlAttribute("container")]
         [XmlAttribute("container")]
         public string Container { get; set; }
         public string Container { get; set; }
 
 
-        public ContainerProfile()
-        {
-            Conditions = Array.Empty<ProfileCondition>();
-        }
-
         public string[] GetContainers()
         public string[] GetContainers()
         {
         {
             return SplitValue(Container);
             return SplitValue(Container);

+ 19 - 17
MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs

@@ -81,13 +81,13 @@ namespace MediaBrowser.Model.Dlna
                             DlnaFlags.DlnaV15;
                             DlnaFlags.DlnaV15;
 
 
             // if (isDirectStream)
             // if (isDirectStream)
-            //{
-            //    flagValue = flagValue | DlnaFlags.ByteBasedSeek;
-            //}
-            // else if (runtimeTicks.HasValue)
-            //{
-            //    flagValue = flagValue | DlnaFlags.TimeBasedSeek;
-            //}
+            // {
+            //     flagValue = flagValue | DlnaFlags.ByteBasedSeek;
+            // }
+            //  else if (runtimeTicks.HasValue)
+            // {
+            //     flagValue = flagValue | DlnaFlags.TimeBasedSeek;
+            // }
 
 
             string dlnaflags = string.Format(
             string dlnaflags = string.Format(
                 CultureInfo.InvariantCulture,
                 CultureInfo.InvariantCulture,
@@ -150,16 +150,18 @@ namespace MediaBrowser.Model.Dlna
                             DlnaFlags.DlnaV15;
                             DlnaFlags.DlnaV15;
 
 
             // if (isDirectStream)
             // if (isDirectStream)
-            //{
-            //    flagValue = flagValue | DlnaFlags.ByteBasedSeek;
-            //}
-            // else if (runtimeTicks.HasValue)
-            //{
-            //    flagValue = flagValue | DlnaFlags.TimeBasedSeek;
-            //}
-
-            string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}",
-             DlnaMaps.FlagsToString(flagValue));
+            // {
+            //     flagValue = flagValue | DlnaFlags.ByteBasedSeek;
+            // }
+            //  else if (runtimeTicks.HasValue)
+            // {
+            //     flagValue = flagValue | DlnaFlags.TimeBasedSeek;
+            // }
+
+            string dlnaflags = string.Format(
+                CultureInfo.InvariantCulture,
+                ";DLNA.ORG_FLAGS={0}",
+                DlnaMaps.FlagsToString(flagValue));
 
 
             ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(
             ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(
                 container,
                 container,

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

@@ -8,6 +8,7 @@ namespace MediaBrowser.Model.Dlna
     public interface IDeviceDiscovery
     public interface IDeviceDiscovery
     {
     {
         event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
         event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
+
         event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
         event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
     }
     }
 }
 }

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

@@ -57,7 +57,6 @@ namespace MediaBrowser.Model.Dlna
                 string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) ||
                 string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) ||
                 string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
                 string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
             {
             {
-
                 return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
                 return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
             }
             }
 
 
@@ -323,7 +322,6 @@ namespace MediaBrowser.Model.Dlna
             if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) &&
             if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) &&
                 (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase)))
                 (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase)))
             {
             {
-
                 if (width.HasValue && height.HasValue)
                 if (width.HasValue && height.HasValue)
                 {
                 {
                     if ((width.Value <= 720) && (height.Value <= 576))
                     if ((width.Value <= 720) && (height.Value <= 576))
@@ -479,7 +477,9 @@ namespace MediaBrowser.Model.Dlna
         {
         {
             if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) ||
             if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) ||
                 string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase))
                 string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase))
+            {
                 return ResolveImageJPGFormat(width, height);
                 return ResolveImageJPGFormat(width, height);
+            }
 
 
             if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase))
             {
             {

+ 4 - 4
MediaBrowser.Model/Dlna/ResolutionConfiguration.cs

@@ -4,14 +4,14 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class ResolutionConfiguration
     public class ResolutionConfiguration
     {
     {
-        public int MaxWidth { get; set; }
-
-        public int MaxBitrate { get; set; }
-
         public ResolutionConfiguration(int maxWidth, int maxBitrate)
         public ResolutionConfiguration(int maxWidth, int maxBitrate)
         {
         {
             MaxWidth = maxWidth;
             MaxWidth = maxWidth;
             MaxBitrate = maxBitrate;
             MaxBitrate = maxBitrate;
         }
         }
+
+        public int MaxWidth { get; set; }
+
+        public int MaxBitrate { get; set; }
     }
     }
 }
 }

+ 5 - 5
MediaBrowser.Model/Dlna/ResponseProfile.cs

@@ -8,6 +8,11 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class ResponseProfile
     public class ResponseProfile
     {
     {
+        public ResponseProfile()
+        {
+            Conditions = Array.Empty<ProfileCondition>();
+        }
+
         [XmlAttribute("container")]
         [XmlAttribute("container")]
         public string Container { get; set; }
         public string Container { get; set; }
 
 
@@ -28,11 +33,6 @@ namespace MediaBrowser.Model.Dlna
 
 
         public ProfileCondition[] Conditions { get; set; }
         public ProfileCondition[] Conditions { get; set; }
 
 
-        public ResponseProfile()
-        {
-            Conditions = Array.Empty<ProfileCondition>();
-        }
-
         public string[] GetContainers()
         public string[] GetContainers()
         {
         {
             return ContainerProfile.SplitValue(Container);
             return ContainerProfile.SplitValue(Container);

+ 27 - 27
MediaBrowser.Model/Dlna/SearchCriteria.cs

@@ -7,31 +7,6 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class SearchCriteria
     public class SearchCriteria
     {
     {
-        public SearchType SearchType { get; set; }
-
-        /// <summary>
-        /// Splits the specified string.
-        /// </summary>
-        /// <param name="str">The string.</param>
-        /// <param name="term">The term.</param>
-        /// <param name="limit">The limit.</param>
-        /// <returns>System.String[].</returns>
-        private static string[] RegexSplit(string str, string term, int limit)
-        {
-            return new Regex(term).Split(str, limit);
-        }
-
-        /// <summary>
-        /// Splits the specified string.
-        /// </summary>
-        /// <param name="str">The string.</param>
-        /// <param name="term">The term.</param>
-        /// <returns>System.String[].</returns>
-        private static string[] RegexSplit(string str, string term)
-        {
-            return Regex.Split(str, term, RegexOptions.IgnoreCase);
-        }
-
         public SearchCriteria(string search)
         public SearchCriteria(string search)
         {
         {
             if (search.Length == 0)
             if (search.Length == 0)
@@ -48,8 +23,8 @@ namespace MediaBrowser.Model.Dlna
 
 
                 if (subFactors.Length == 3)
                 if (subFactors.Length == 3)
                 {
                 {
-                    if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase) &&
-                        (string.Equals("=", subFactors[1], StringComparison.Ordinal) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase)))
+                    if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase)
+                        && (string.Equals("=", subFactors[1], StringComparison.Ordinal) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase)))
                     {
                     {
                         if (string.Equals("\"object.item.imageItem\"", subFactors[2], StringComparison.Ordinal) || string.Equals("\"object.item.imageItem.photo\"", subFactors[2], StringComparison.OrdinalIgnoreCase))
                         if (string.Equals("\"object.item.imageItem\"", subFactors[2], StringComparison.Ordinal) || string.Equals("\"object.item.imageItem.photo\"", subFactors[2], StringComparison.OrdinalIgnoreCase))
                         {
                         {
@@ -71,5 +46,30 @@ namespace MediaBrowser.Model.Dlna
                 }
                 }
             }
             }
         }
         }
+
+        public SearchType SearchType { get; set; }
+
+        /// <summary>
+        /// Splits the specified string.
+        /// </summary>
+        /// <param name="str">The string.</param>
+        /// <param name="term">The term.</param>
+        /// <param name="limit">The limit.</param>
+        /// <returns>System.String[].</returns>
+        private static string[] RegexSplit(string str, string term, int limit)
+        {
+            return new Regex(term).Split(str, limit);
+        }
+
+        /// <summary>
+        /// Splits the specified string.
+        /// </summary>
+        /// <param name="str">The string.</param>
+        /// <param name="term">The term.</param>
+        /// <returns>System.String[].</returns>
+        private static string[] RegexSplit(string str, string term)
+        {
+            return Regex.Split(str, term, RegexOptions.IgnoreCase);
+        }
     }
     }
 }
 }

+ 2 - 2
MediaBrowser.Model/Dlna/SortCriteria.cs

@@ -6,10 +6,10 @@ namespace MediaBrowser.Model.Dlna
 {
 {
     public class SortCriteria
     public class SortCriteria
     {
     {
-        public SortOrder SortOrder => SortOrder.Ascending;
-
         public SortCriteria(string value)
         public SortCriteria(string value)
         {
         {
         }
         }
+
+        public SortOrder SortOrder => SortOrder.Ascending;
     }
     }
 }
 }

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

@@ -227,7 +227,7 @@ namespace MediaBrowser.Model.Dlna
             }
             }
         }
         }
 
 
-        public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, string _, DeviceProfile profile, DlnaProfileType type)
+        public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type)
         {
         {
             if (string.IsNullOrEmpty(inputContainer))
             if (string.IsNullOrEmpty(inputContainer))
             {
             {
@@ -274,14 +274,14 @@ namespace MediaBrowser.Model.Dlna
             if (options.ForceDirectPlay)
             if (options.ForceDirectPlay)
             {
             {
                 playlistItem.PlayMethod = PlayMethod.DirectPlay;
                 playlistItem.PlayMethod = PlayMethod.DirectPlay;
-                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
                 return playlistItem;
                 return playlistItem;
             }
             }
 
 
             if (options.ForceDirectStream)
             if (options.ForceDirectStream)
             {
             {
                 playlistItem.PlayMethod = PlayMethod.DirectStream;
                 playlistItem.PlayMethod = PlayMethod.DirectStream;
-                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
                 return playlistItem;
                 return playlistItem;
             }
             }
 
 
@@ -349,7 +349,7 @@ namespace MediaBrowser.Model.Dlna
                         playlistItem.PlayMethod = PlayMethod.DirectStream;
                         playlistItem.PlayMethod = PlayMethod.DirectStream;
                     }
                     }
 
 
-                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
 
 
                     return playlistItem;
                     return playlistItem;
                 }
                 }
@@ -698,7 +698,7 @@ namespace MediaBrowser.Model.Dlna
                 if (directPlay != null)
                 if (directPlay != null)
                 {
                 {
                     playlistItem.PlayMethod = directPlay.Value;
                     playlistItem.PlayMethod = directPlay.Value;
-                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Video);
+                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video);
 
 
                     if (subtitleStream != null)
                     if (subtitleStream != null)
                     {
                     {
@@ -1404,7 +1404,9 @@ namespace MediaBrowser.Model.Dlna
             {
             {
                 _logger.LogInformation(
                 _logger.LogInformation(
                     "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
                     "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
-                    playMethod, itemBitrate, requestedMaxBitrate);
+                    playMethod,
+                    itemBitrate,
+                    requestedMaxBitrate);
                 return false;
                 return false;
             }
             }
 
 

+ 577 - 597
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -27,45 +27,6 @@ namespace MediaBrowser.Model.Dlna
             StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
         }
 
 
-        public void SetOption(string qualifier, string name, string value)
-        {
-            if (string.IsNullOrEmpty(qualifier))
-            {
-                SetOption(name, value);
-            }
-            else
-            {
-                SetOption(qualifier + "-" + name, value);
-            }
-        }
-
-        public void SetOption(string name, string value)
-        {
-            StreamOptions[name] = value;
-        }
-
-        public string GetOption(string qualifier, string name)
-        {
-            var value = GetOption(qualifier + "-" + name);
-
-            if (string.IsNullOrEmpty(value))
-            {
-                value = GetOption(name);
-            }
-
-            return value;
-        }
-
-        public string GetOption(string name)
-        {
-            if (StreamOptions.TryGetValue(name, out var value))
-            {
-                return value;
-            }
-
-            return null;
-        }
-
         public Guid ItemId { get; set; }
         public Guid ItemId { get; set; }
 
 
         public PlayMethod PlayMethod { get; set; }
         public PlayMethod PlayMethod { get; set; }
@@ -152,887 +113,906 @@ namespace MediaBrowser.Model.Dlna
             PlayMethod == PlayMethod.DirectStream ||
             PlayMethod == PlayMethod.DirectStream ||
             PlayMethod == PlayMethod.DirectPlay;
             PlayMethod == PlayMethod.DirectPlay;
 
 
-        public string ToUrl(string baseUrl, string accessToken)
-        {
-            if (PlayMethod == PlayMethod.DirectPlay)
-            {
-                return MediaSource.Path;
-            }
+        /// <summary>
+        /// Gets the audio stream that will be used.
+        /// </summary>
+        public MediaStream TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
 
 
-            if (string.IsNullOrEmpty(baseUrl))
+        /// <summary>
+        /// Gets the video stream that will be used.
+        /// </summary>
+        public MediaStream TargetVideoStream => MediaSource?.VideoStream;
+
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public int? TargetAudioSampleRate
+        {
+            get
             {
             {
-                throw new ArgumentNullException(nameof(baseUrl));
+                var stream = TargetAudioStream;
+                return AudioSampleRate.HasValue && !IsDirectStream
+                    ? AudioSampleRate
+                    : stream == null ? null : stream.SampleRate;
             }
             }
+        }
 
 
-            var list = new List<string>();
-            foreach (NameValuePair pair in BuildParams(this, accessToken))
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public int? TargetAudioBitDepth
+        {
+            get
             {
             {
-                if (string.IsNullOrEmpty(pair.Value))
+                if (IsDirectStream)
                 {
                 {
-                    continue;
+                    return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
                 }
                 }
 
 
-                // Try to keep the url clean by omitting defaults
-                if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) &&
-                    string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
+                var targetAudioCodecs = TargetAudioCodec;
+                var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+                if (!string.IsNullOrEmpty(audioCodec))
                 {
                 {
-                    continue;
+                    return GetTargetAudioBitDepth(audioCodec);
                 }
                 }
 
 
-                if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) &&
-                    string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
+                return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
+            }
+        }
+
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public int? TargetVideoBitDepth
+        {
+            get
+            {
+                if (IsDirectStream)
                 {
                 {
-                    continue;
+                    return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
                 }
                 }
 
 
-                // Be careful, IsDirectStream==true by default (Static != false or not in query).
-                // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
-                if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
-                    string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase))
+                var targetVideoCodecs = TargetVideoCodec;
+                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+                if (!string.IsNullOrEmpty(videoCodec))
                 {
                 {
-                    continue;
+                    return GetTargetVideoBitDepth(videoCodec);
                 }
                 }
 
 
-                var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
-
-                list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+                return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
             }
             }
-
-            string queryString = string.Join('&', list);
-
-            return GetUrl(baseUrl, queryString);
         }
         }
 
 
-        private string GetUrl(string baseUrl, string queryString)
+        /// <summary>
+        /// Gets the target reference frames.
+        /// </summary>
+        /// <value>The target reference frames.</value>
+        public int? TargetRefFrames
         {
         {
-            if (string.IsNullOrEmpty(baseUrl))
-            {
-                throw new ArgumentNullException(nameof(baseUrl));
-            }
-
-            string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
-
-            baseUrl = baseUrl.TrimEnd('/');
-
-            if (MediaType == DlnaProfileType.Audio)
+            get
             {
             {
-                if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+                if (IsDirectStream)
                 {
                 {
-                    return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+                    return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
                 }
                 }
 
 
-                return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
-            }
+                var targetVideoCodecs = TargetVideoCodec;
+                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+                if (!string.IsNullOrEmpty(videoCodec))
+                {
+                    return GetTargetRefFrames(videoCodec);
+                }
 
 
-            if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
-            {
-                return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+                return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
             }
             }
-
-            return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
         }
         }
 
 
-        private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public float? TargetFramerate
         {
         {
-            var list = new List<NameValuePair>();
-
-            string audioCodecs = item.AudioCodecs.Length == 0 ?
-                string.Empty :
-                string.Join(',', item.AudioCodecs);
-
-            string videoCodecs = item.VideoCodecs.Length == 0 ?
-                string.Empty :
-                string.Join(',', item.VideoCodecs);
-
-            list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
-            list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
-            list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
-            list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-            list.Add(new NameValuePair("VideoCodec", videoCodecs));
-            list.Add(new NameValuePair("AudioCodec", audioCodecs));
-            list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
-            list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-            list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
-            long startPositionTicks = item.StartPositionTicks;
-
-            var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase);
-
-            if (isHls)
-            {
-                list.Add(new NameValuePair("StartTimeTicks", string.Empty));
-            }
-            else
+            get
             {
             {
-                list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+                var stream = TargetVideoStream;
+                return MaxFramerate.HasValue && !IsDirectStream
+                    ? MaxFramerate
+                    : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate;
             }
             }
+        }
 
 
-            list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
-            list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
-
-            string liveStreamId = item.MediaSource?.LiveStreamId;
-            list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
-
-            list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
-
-            if (!item.IsDirectStream)
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public double? TargetVideoLevel
+        {
+            get
             {
             {
-                if (item.RequireNonAnamorphic)
+                if (IsDirectStream)
                 {
                 {
-                    list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+                    return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
                 }
                 }
 
 
-                list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
-                if (item.EnableSubtitlesInManifest)
+                var targetVideoCodecs = TargetVideoCodec;
+                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+                if (!string.IsNullOrEmpty(videoCodec))
                 {
                 {
-                    list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+                    return GetTargetVideoLevel(videoCodec);
                 }
                 }
 
 
-                if (item.EnableMpegtsM2TsMode)
-                {
-                    list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
+                return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
+            }
+        }
 
 
-                if (item.EstimateContentLength)
-                {
-                    list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public int? TargetPacketLength
+        {
+            get
+            {
+                var stream = TargetVideoStream;
+                return !IsDirectStream
+                    ? null
+                    : stream == null ? null : stream.PacketLength;
+            }
+        }
 
 
-                if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public string TargetVideoProfile
+        {
+            get
+            {
+                if (IsDirectStream)
                 {
                 {
-                    list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+                    return TargetVideoStream == null ? null : TargetVideoStream.Profile;
                 }
                 }
 
 
-                if (item.CopyTimestamps)
+                var targetVideoCodecs = TargetVideoCodec;
+                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+                if (!string.IsNullOrEmpty(videoCodec))
                 {
                 {
-                    list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+                    return GetOption(videoCodec, "profile");
                 }
                 }
 
 
-                list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-            }
-
-            list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
-
-            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));
-
-            if (isHls)
-            {
-                list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
-
-                if (item.SegmentLength.HasValue)
-                {
-                    list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
-                }
-
-                if (item.MinSegments.HasValue)
-                {
-                    list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
-                }
-
-                list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
-            }
-
-            foreach (var pair in item.StreamOptions)
-            {
-                if (string.IsNullOrEmpty(pair.Value))
-                {
-                    continue;
-                }
-
-                // strip spaces to avoid having to encode h264 profile names
-                list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
+                return TargetVideoStream == null ? null : TargetVideoStream.Profile;
             }
             }
+        }
 
 
-            if (!item.IsDirectStream)
+        /// <summary>
+        /// Gets the target video codec tag.
+        /// </summary>
+        /// <value>The target video codec tag.</value>
+        public string TargetVideoCodecTag
+        {
+            get
             {
             {
-                list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct().Select(i => i.ToString()))));
+                var stream = TargetVideoStream;
+                return !IsDirectStream
+                    ? null
+                    : stream == null ? null : stream.CodecTag;
             }
             }
-
-            return list;
         }
         }
 
 
-        public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+        /// <summary>
+        /// Gets the audio bitrate that will be in the output stream.
+        /// </summary>
+        public int? TargetAudioBitrate
         {
         {
-            return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+            get
+            {
+                var stream = TargetAudioStream;
+                return AudioBitrate.HasValue && !IsDirectStream
+                    ? AudioBitrate
+                    : stream == null ? null : stream.BitRate;
+            }
         }
         }
 
 
-        public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+        /// <summary>
+        /// Gets the audio channels that will be in the output stream.
+        /// </summary>
+        public int? TargetAudioChannels
         {
         {
-            var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
-            var newList = new List<SubtitleStreamInfo>();
-
-            // First add the selected track
-            foreach (SubtitleStreamInfo stream in list)
+            get
             {
             {
-                if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
+                if (IsDirectStream)
                 {
                 {
-                    newList.Add(stream);
+                    return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
                 }
                 }
-            }
 
 
-            return newList;
-        }
+                var targetAudioCodecs = TargetAudioCodec;
+                var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+                if (!string.IsNullOrEmpty(codec))
+                {
+                    return GetTargetRefFrames(codec);
+                }
 
 
-        public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
-        {
-            return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+                return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+            }
         }
         }
 
 
-        public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+        /// <summary>
+        /// Gets the audio codec that will be in the output stream.
+        /// </summary>
+        public string[] TargetAudioCodec
         {
         {
-            var list = new List<SubtitleStreamInfo>();
+            get
+            {
+                var stream = TargetAudioStream;
 
 
-            // HLS will preserve timestamps so we can just grab the full subtitle stream
-            long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)
-                ? 0
-                : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
+                string inputCodec = stream?.Codec;
 
 
-            // First add the selected track
-            if (SubtitleStreamIndex.HasValue)
-            {
-                foreach (var stream in MediaSource.MediaStreams)
+                if (IsDirectStream)
                 {
                 {
-                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
-                    {
-                        AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
-                    }
+                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
                 }
                 }
-            }
 
 
-            if (!includeSelectedTrackOnly)
-            {
-                foreach (var stream in MediaSource.MediaStreams)
+                foreach (string codec in AudioCodecs)
                 {
                 {
-                    if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
+                    if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
+                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
                     }
                     }
                 }
                 }
-            }
-
-            return list;
-        }
-
-        private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
-        {
-            if (enableAllProfiles)
-            {
-                foreach (var profile in DeviceProfile.SubtitleProfiles)
-                {
-                    var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
-
-                    list.Add(info);
-                }
-            }
-            else
-            {
-                var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
 
 
-                list.Add(info);
+                return AudioCodecs;
             }
             }
         }
         }
 
 
-        private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+        public string[] TargetVideoCodec
         {
         {
-            var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
-            var info = new SubtitleStreamInfo
+            get
             {
             {
-                IsForced = stream.IsForced,
-                Language = stream.Language,
-                Name = stream.Language ?? "Unknown",
-                Format = subtitleProfile.Format,
-                Index = stream.Index,
-                DeliveryMethod = subtitleProfile.Method,
-                DisplayTitle = stream.DisplayTitle
-            };
+                var stream = TargetVideoStream;
 
 
-            if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
-            {
-                if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
+                string inputCodec = stream?.Codec;
+
+                if (IsDirectStream)
                 {
                 {
-                    info.Url = string.Format(CultureInfo.InvariantCulture, "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
-                        baseUrl,
-                        ItemId,
-                        MediaSourceId,
-                        stream.Index.ToString(CultureInfo.InvariantCulture),
-                        startPositionTicks.ToString(CultureInfo.InvariantCulture),
-                        subtitleProfile.Format);
+                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
+                }
 
 
-                    if (!string.IsNullOrEmpty(accessToken))
+                foreach (string codec in VideoCodecs)
+                {
+                    if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        info.Url += "?api_key=" + accessToken;
+                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
                     }
                     }
-
-                    info.IsExternalUrl = false;
-                }
-                else
-                {
-                    info.Url = stream.Path;
-                    info.IsExternalUrl = true;
                 }
                 }
-            }
 
 
-            return info;
+                return VideoCodecs;
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Returns the audio stream that will be used.
+        /// Gets the audio channels that will be in the output stream.
         /// </summary>
         /// </summary>
-        public MediaStream TargetAudioStream
+        public long? TargetSize
         {
         {
             get
             get
             {
             {
-                if (MediaSource != null)
+                if (IsDirectStream)
+                {
+                    return MediaSource.Size;
+                }
+
+                if (RunTimeTicks.HasValue)
                 {
                 {
-                    return MediaSource.GetDefaultAudioStream(AudioStreamIndex);
+                    int? totalBitrate = TargetTotalBitrate;
+
+                    double totalSeconds = RunTimeTicks.Value;
+                    // Convert to ms
+                    totalSeconds /= 10000;
+                    // Convert to seconds
+                    totalSeconds /= 1000;
+
+                    return totalBitrate.HasValue ?
+                        Convert.ToInt64(totalBitrate.Value * totalSeconds) :
+                        (long?)null;
                 }
                 }
 
 
                 return null;
                 return null;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Returns the video stream that will be used.
-        /// </summary>
-        public MediaStream TargetVideoStream
+        public int? TargetVideoBitrate
         {
         {
             get
             get
             {
             {
-                if (MediaSource != null)
-                {
-                    return MediaSource.VideoStream;
-                }
+                var stream = TargetVideoStream;
 
 
-                return null;
+                return VideoBitrate.HasValue && !IsDirectStream
+                    ? VideoBitrate
+                    : stream == null ? null : stream.BitRate;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioSampleRate
+        public TransportStreamTimestamp TargetTimestamp
         {
         {
             get
             get
             {
             {
-                var stream = TargetAudioStream;
-                return AudioSampleRate.HasValue && !IsDirectStream
-                    ? AudioSampleRate
-                    : stream == null ? null : stream.SampleRate;
+                var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
+                    ? TransportStreamTimestamp.Valid
+                    : TransportStreamTimestamp.None;
+
+                return !IsDirectStream
+                    ? defaultValue
+                    : MediaSource == null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioBitDepth
+        public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
+
+        public bool? IsTargetAnamorphic
         {
         {
             get
             get
             {
             {
                 if (IsDirectStream)
                 if (IsDirectStream)
                 {
                 {
-                    return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
-                }
-
-                var targetAudioCodecs = TargetAudioCodec;
-                var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
-                if (!string.IsNullOrEmpty(audioCodec))
-                {
-                    return GetTargetAudioBitDepth(audioCodec);
+                    return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic;
                 }
                 }
 
 
-                return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
+                return false;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetVideoBitDepth
+        public bool? IsTargetInterlaced
         {
         {
             get
             get
             {
             {
                 if (IsDirectStream)
                 if (IsDirectStream)
                 {
                 {
-                    return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+                    return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
                 }
                 }
 
 
                 var targetVideoCodecs = TargetVideoCodec;
                 var targetVideoCodecs = TargetVideoCodec;
                 var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
                 var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
                 if (!string.IsNullOrEmpty(videoCodec))
                 if (!string.IsNullOrEmpty(videoCodec))
                 {
                 {
-                    return GetTargetVideoBitDepth(videoCodec);
-                }
+                    if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
+                }
 
 
-                return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+                return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Gets the target reference frames.
-        /// </summary>
-        /// <value>The target reference frames.</value>
-        public int? TargetRefFrames
+        public bool? IsTargetAVC
         {
         {
             get
             get
             {
             {
                 if (IsDirectStream)
                 if (IsDirectStream)
                 {
                 {
-                    return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
+                    return TargetVideoStream == null ? null : TargetVideoStream.IsAVC;
                 }
                 }
 
 
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
+                return true;
+            }
+        }
+
+        public int? TargetWidth
+        {
+            get
+            {
+                var videoStream = TargetVideoStream;
+
+                if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue)
                 {
                 {
-                    return GetTargetRefFrames(videoCodec);
+                    ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
+
+                    size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+
+                    return size.Width;
                 }
                 }
 
 
-                return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
+                return MaxWidth;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public float? TargetFramerate
+        public int? TargetHeight
         {
         {
             get
             get
             {
             {
-                var stream = TargetVideoStream;
-                return MaxFramerate.HasValue && !IsDirectStream
-                    ? MaxFramerate
-                    : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate;
+                var videoStream = TargetVideoStream;
+
+                if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue)
+                {
+                    ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
+
+                    size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+
+                    return size.Height;
+                }
+
+                return MaxHeight;
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public double? TargetVideoLevel
+        public int? TargetVideoStreamCount
         {
         {
             get
             get
             {
             {
                 if (IsDirectStream)
                 if (IsDirectStream)
                 {
                 {
-                    return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
-                }
-
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
-                {
-                    return GetTargetVideoLevel(videoCodec);
+                    return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
                 }
                 }
 
 
-                return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
+                return GetMediaStreamCount(MediaStreamType.Video, 1);
             }
             }
         }
         }
 
 
-        public int? GetTargetVideoBitDepth(string codec)
+        public int? TargetAudioStreamCount
         {
         {
-            var value = GetOption(codec, "videobitdepth");
-            if (string.IsNullOrEmpty(value))
+            get
             {
             {
-                return null;
-            }
+                if (IsDirectStream)
+                {
+                    return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
+                }
 
 
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
-            {
-                return result;
+                return GetMediaStreamCount(MediaStreamType.Audio, 1);
             }
             }
-
-            return null;
         }
         }
 
 
-        public int? GetTargetAudioBitDepth(string codec)
+        public void SetOption(string qualifier, string name, string value)
         {
         {
-            var value = GetOption(codec, "audiobitdepth");
-            if (string.IsNullOrEmpty(value))
+            if (string.IsNullOrEmpty(qualifier))
             {
             {
-                return null;
+                SetOption(name, value);
             }
             }
-
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            else
             {
             {
-                return result;
+                SetOption(qualifier + "-" + name, value);
             }
             }
+        }
 
 
-            return null;
+        public void SetOption(string name, string value)
+        {
+            StreamOptions[name] = value;
         }
         }
 
 
-        public double? GetTargetVideoLevel(string codec)
+        public string GetOption(string qualifier, string name)
         {
         {
-            var value = GetOption(codec, "level");
+            var value = GetOption(qualifier + "-" + name);
+
             if (string.IsNullOrEmpty(value))
             if (string.IsNullOrEmpty(value))
             {
             {
-                return null;
+                value = GetOption(name);
             }
             }
 
 
-            if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+            return value;
+        }
+
+        public string GetOption(string name)
+        {
+            if (StreamOptions.TryGetValue(name, out var value))
             {
             {
-                return result;
+                return value;
             }
             }
 
 
             return null;
             return null;
         }
         }
 
 
-        public int? GetTargetRefFrames(string codec)
+        public string ToUrl(string baseUrl, string accessToken)
         {
         {
-            var value = GetOption(codec, "maxrefframes");
-            if (string.IsNullOrEmpty(value))
+            if (PlayMethod == PlayMethod.DirectPlay)
             {
             {
-                return null;
+                return MediaSource.Path;
             }
             }
 
 
-            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+            if (string.IsNullOrEmpty(baseUrl))
             {
             {
-                return result;
+                throw new ArgumentNullException(nameof(baseUrl));
             }
             }
 
 
-            return null;
-        }
-
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public int? TargetPacketLength
-        {
-            get
+            var list = new List<string>();
+            foreach (NameValuePair pair in BuildParams(this, accessToken))
             {
             {
-                var stream = TargetVideoStream;
-                return !IsDirectStream
-                    ? null
-                    : stream == null ? null : stream.PacketLength;
-            }
-        }
+                if (string.IsNullOrEmpty(pair.Value))
+                {
+                    continue;
+                }
 
 
-        /// <summary>
-        /// Predicts the audio sample rate that will be in the output stream.
-        /// </summary>
-        public string TargetVideoProfile
-        {
-            get
-            {
-                if (IsDirectStream)
+                // Try to keep the url clean by omitting defaults
+                if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) &&
+                    string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    return TargetVideoStream == null ? null : TargetVideoStream.Profile;
+                    continue;
                 }
                 }
 
 
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
+                if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) &&
+                    string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    return GetOption(videoCodec, "profile");
+                    continue;
                 }
                 }
 
 
-                return TargetVideoStream == null ? null : TargetVideoStream.Profile;
-            }
-        }
+                // Be careful, IsDirectStream==true by default (Static != false or not in query).
+                // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
+                if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
+                    string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
 
 
-        /// <summary>
-        /// Gets the target video codec tag.
-        /// </summary>
-        /// <value>The target video codec tag.</value>
-        public string TargetVideoCodecTag
-        {
-            get
-            {
-                var stream = TargetVideoStream;
-                return !IsDirectStream
-                    ? null
-                    : stream == null ? null : stream.CodecTag;
+                var encodedValue = pair.Value.Replace(" ", "%20");
+
+                list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
             }
             }
+
+            string queryString = string.Join("&", list.ToArray());
+
+            return GetUrl(baseUrl, queryString);
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio bitrate that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioBitrate
+        private string GetUrl(string baseUrl, string queryString)
         {
         {
-            get
+            if (string.IsNullOrEmpty(baseUrl))
             {
             {
-                var stream = TargetAudioStream;
-                return AudioBitrate.HasValue && !IsDirectStream
-                    ? AudioBitrate
-                    : stream == null ? null : stream.BitRate;
+                throw new ArgumentNullException(nameof(baseUrl));
             }
             }
-        }
 
 
-        /// <summary>
-        /// Predicts the audio channels that will be in the output stream.
-        /// </summary>
-        public int? TargetAudioChannels
-        {
-            get
-            {
-                if (IsDirectStream)
-                {
-                    return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
-                }
+            string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
 
 
-                var targetAudioCodecs = TargetAudioCodec;
-                var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
-                if (!string.IsNullOrEmpty(codec))
+            baseUrl = baseUrl.TrimEnd('/');
+
+            if (MediaType == DlnaProfileType.Audio)
+            {
+                if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    return GetTargetRefFrames(codec);
+                    return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
                 }
                 }
 
 
-                return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
-            }
-        }
-
-        public int? GetTargetAudioChannels(string codec)
-        {
-            var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
-
-            var value = GetOption(codec, "audiochannels");
-            if (string.IsNullOrEmpty(value))
-            {
-                return defaultValue;
+                return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
             }
             }
 
 
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
             {
-                return Math.Min(result, defaultValue ?? result);
+                return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
             }
             }
 
 
-            return defaultValue;
+            return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
         }
         }
 
 
-        /// <summary>
-        /// Predicts the audio codec that will be in the output stream.
-        /// </summary>
-        public string[] TargetAudioCodec
+        private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
         {
         {
-            get
+            var list = new List<NameValuePair>();
+
+            string audioCodecs = item.AudioCodecs.Length == 0 ?
+                string.Empty :
+                string.Join(",", item.AudioCodecs);
+
+            string videoCodecs = item.VideoCodecs.Length == 0 ?
+                string.Empty :
+                string.Join(",", item.VideoCodecs);
+
+            list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+            list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+            list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+            list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+            list.Add(new NameValuePair("VideoCodec", videoCodecs));
+            list.Add(new NameValuePair("AudioCodec", audioCodecs));
+            list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+            list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+            list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+            list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+            list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+            list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+            list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+            list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+            long startPositionTicks = item.StartPositionTicks;
+
+            var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase);
+
+            if (isHls)
             {
             {
-                var stream = TargetAudioStream;
+                list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+            }
+            else
+            {
+                list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+            }
 
 
-                string inputCodec = stream?.Codec;
+            list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+            list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
 
 
-                if (IsDirectStream)
+            string liveStreamId = item.MediaSource?.LiveStreamId;
+            list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+
+            list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
+
+            if (!item.IsDirectStream)
+            {
+                if (item.RequireNonAnamorphic)
                 {
                 {
-                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
+                    list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
                 }
                 }
 
 
-                foreach (string codec in AudioCodecs)
+                list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+                if (item.EnableSubtitlesInManifest)
                 {
                 {
-                    if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
-                    }
+                    list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
                 }
                 }
 
 
-                return AudioCodecs;
-            }
-        }
-
-        public string[] TargetVideoCodec
-        {
-            get
-            {
-                var stream = TargetVideoStream;
+                if (item.EnableMpegtsM2TsMode)
+                {
+                    list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+                }
 
 
-                string inputCodec = stream?.Codec;
+                if (item.EstimateContentLength)
+                {
+                    list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+                }
 
 
-                if (IsDirectStream)
+                if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
                 {
                 {
-                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
+                    list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
                 }
                 }
 
 
-                foreach (string codec in VideoCodecs)
+                if (item.CopyTimestamps)
                 {
                 {
-                    if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
-                    }
+                    list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
                 }
                 }
 
 
-                return VideoCodecs;
+                list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
             }
             }
-        }
 
 
-        /// <summary>
-        /// Predicts the audio channels that will be in the output stream.
-        /// </summary>
-        public long? TargetSize
-        {
-            get
+            list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
+
+            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));
+
+            if (isHls)
             {
             {
-                if (IsDirectStream)
+                list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+
+                if (item.SegmentLength.HasValue)
                 {
                 {
-                    return MediaSource.Size;
+                    list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
                 }
                 }
 
 
-                if (RunTimeTicks.HasValue)
+                if (item.MinSegments.HasValue)
                 {
                 {
-                    int? totalBitrate = TargetTotalBitrate;
-
-                    double totalSeconds = RunTimeTicks.Value;
-                    // Convert to ms
-                    totalSeconds /= 10000;
-                    // Convert to seconds
-                    totalSeconds /= 1000;
-
-                    return totalBitrate.HasValue ?
-                        Convert.ToInt64(totalBitrate.Value * totalSeconds) :
-                        (long?)null;
+                    list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
                 }
                 }
 
 
-                return null;
+                list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
             }
             }
-        }
 
 
-        public int? TargetVideoBitrate
-        {
-            get
+            foreach (var pair in item.StreamOptions)
             {
             {
-                var stream = TargetVideoStream;
+                if (string.IsNullOrEmpty(pair.Value))
+                {
+                    continue;
+                }
 
 
-                return VideoBitrate.HasValue && !IsDirectStream
-                    ? VideoBitrate
-                    : stream == null ? null : stream.BitRate;
+                // strip spaces to avoid having to encode h264 profile names
+                list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty)));
             }
             }
-        }
 
 
-        public TransportStreamTimestamp TargetTimestamp
-        {
-            get
+            if (!item.IsDirectStream)
             {
             {
-                var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
-                    ? TransportStreamTimestamp.Valid
-                    : TransportStreamTimestamp.None;
-
-                return !IsDirectStream
-                    ? defaultValue
-                    : MediaSource == null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
+                list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct())));
             }
             }
+
+            return list;
         }
         }
 
 
-        public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
+        public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+        {
+            return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+        }
 
 
-        public bool? IsTargetAnamorphic
+        public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
         {
         {
-            get
+            var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
+            var newList = new List<SubtitleStreamInfo>();
+
+            // First add the selected track
+            foreach (SubtitleStreamInfo stream in list)
             {
             {
-                if (IsDirectStream)
+                if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
                 {
                 {
-                    return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic;
+                    newList.Add(stream);
                 }
                 }
-
-                return false;
             }
             }
+
+            return newList;
         }
         }
 
 
-        public bool? IsTargetInterlaced
+        public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
         {
         {
-            get
+            return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+        }
+
+        public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+        {
+            var list = new List<SubtitleStreamInfo>();
+
+            // HLS will preserve timestamps so we can just grab the full subtitle stream
+            long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)
+                ? 0
+                : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
+
+            // First add the selected track
+            if (SubtitleStreamIndex.HasValue)
             {
             {
-                if (IsDirectStream)
+                foreach (var stream in MediaSource.MediaStreams)
                 {
                 {
-                    return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
+                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
+                    {
+                        AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
+                    }
                 }
                 }
+            }
 
 
-                var targetVideoCodecs = TargetVideoCodec;
-                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
-                if (!string.IsNullOrEmpty(videoCodec))
+            if (!includeSelectedTrackOnly)
+            {
+                foreach (var stream in MediaSource.MediaStreams)
                 {
                 {
-                    if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+                    if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
                     {
                     {
-                        return false;
+                        AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
                     }
                     }
                 }
                 }
-
-                return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
             }
             }
+
+            return list;
         }
         }
 
 
-        public bool? IsTargetAVC
+        private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
         {
         {
-            get
+            if (enableAllProfiles)
             {
             {
-                if (IsDirectStream)
+                foreach (var profile in DeviceProfile.SubtitleProfiles)
                 {
                 {
-                    return TargetVideoStream == null ? null : TargetVideoStream.IsAVC;
+                    var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
+
+                    list.Add(info);
                 }
                 }
+            }
+            else
+            {
+                var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
 
 
-                return true;
+                list.Add(info);
             }
             }
         }
         }
 
 
-        public int? TargetWidth
+        private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
         {
         {
-            get
+            var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
+            var info = new SubtitleStreamInfo
             {
             {
-                var videoStream = TargetVideoStream;
+                IsForced = stream.IsForced,
+                Language = stream.Language,
+                Name = stream.Language ?? "Unknown",
+                Format = subtitleProfile.Format,
+                Index = stream.Index,
+                DeliveryMethod = subtitleProfile.Method,
+                DisplayTitle = stream.DisplayTitle
+            };
 
 
-                if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue)
+            if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+            {
+                if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
                 {
                 {
-                    ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
+                    info.Url = string.Format(
+                        CultureInfo.InvariantCulture,
+                        "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+                        baseUrl,
+                        ItemId,
+                        MediaSourceId,
+                        stream.Index.ToString(CultureInfo.InvariantCulture),
+                        startPositionTicks.ToString(CultureInfo.InvariantCulture),
+                        subtitleProfile.Format);
 
 
-                    size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+                    if (!string.IsNullOrEmpty(accessToken))
+                    {
+                        info.Url += "?api_key=" + accessToken;
+                    }
 
 
-                    return size.Width;
+                    info.IsExternalUrl = false;
                 }
                 }
+                else
+                {
+                    info.Url = stream.Path;
+                    info.IsExternalUrl = true;
+                }
+            }
 
 
-                return MaxWidth;
+            return info;
+        }
+
+        public int? GetTargetVideoBitDepth(string codec)
+        {
+            var value = GetOption(codec, "videobitdepth");
+            if (string.IsNullOrEmpty(value))
+            {
+                return null;
+            }
+
+            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
             }
             }
+
+            return null;
         }
         }
 
 
-        public int? TargetHeight
+        public int? GetTargetAudioBitDepth(string codec)
         {
         {
-            get
+            var value = GetOption(codec, "audiobitdepth");
+            if (string.IsNullOrEmpty(value))
             {
             {
-                var videoStream = TargetVideoStream;
+                return null;
+            }
 
 
-                if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue)
-                {
-                    ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
+            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
+            }
 
 
-                    size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+            return null;
+        }
 
 
-                    return size.Height;
-                }
+        public double? GetTargetVideoLevel(string codec)
+        {
+            var value = GetOption(codec, "level");
+            if (string.IsNullOrEmpty(value))
+            {
+                return null;
+            }
 
 
-                return MaxHeight;
+            if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
             }
             }
+
+            return null;
         }
         }
 
 
-        public int? TargetVideoStreamCount
+        public int? GetTargetRefFrames(string codec)
         {
         {
-            get
+            var value = GetOption(codec, "maxrefframes");
+            if (string.IsNullOrEmpty(value))
             {
             {
-                if (IsDirectStream)
-                {
-                    return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
-                }
+                return null;
+            }
 
 
-                return GetMediaStreamCount(MediaStreamType.Video, 1);
+            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+            {
+                return result;
             }
             }
+
+            return null;
         }
         }
 
 
-        public int? TargetAudioStreamCount
+        public int? GetTargetAudioChannels(string codec)
         {
         {
-            get
+            var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
+
+            var value = GetOption(codec, "audiochannels");
+            if (string.IsNullOrEmpty(value))
             {
             {
-                if (IsDirectStream)
-                {
-                    return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
-                }
+                return defaultValue;
+            }
 
 
-                return GetMediaStreamCount(MediaStreamType.Audio, 1);
+            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            {
+                return Math.Min(result, defaultValue ?? result);
             }
             }
+
+            return defaultValue;
         }
         }
 
 
         private int? GetMediaStreamCount(MediaStreamType type, int limit)
         private int? GetMediaStreamCount(MediaStreamType type, int limit)

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

@@ -294,13 +294,13 @@ namespace MediaBrowser.Model.Dto
         public NameGuidPair[] GenreItems { get; set; }
         public NameGuidPair[] GenreItems { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// If the item does not have a logo, this will hold the Id of the Parent that has one.
+        /// Gets or sets wether the item has a logo, this will hold the Id of the Parent that has one.
         /// </summary>
         /// </summary>
         /// <value>The parent logo item id.</value>
         /// <value>The parent logo item id.</value>
         public string ParentLogoItemId { get; set; }
         public string ParentLogoItemId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+        /// Gets or sets wether the item has any backdrops, this will hold the Id of the Parent that has one.
         /// </summary>
         /// </summary>
         /// <value>The parent backdrop item id.</value>
         /// <value>The parent backdrop item id.</value>
         public string ParentBackdropItemId { get; set; }
         public string ParentBackdropItemId { get; set; }
@@ -318,7 +318,7 @@ namespace MediaBrowser.Model.Dto
         public int? LocalTrailerCount { get; set; }
         public int? LocalTrailerCount { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// User data for this item based on the user it's being requested for.
+        /// Gets or sets the user data for this item based on the user it's being requested for.
         /// </summary>
         /// </summary>
         /// <value>The user data.</value>
         /// <value>The user data.</value>
         public UserItemDataDto UserData { get; set; }
         public UserItemDataDto UserData { get; set; }
@@ -506,7 +506,7 @@ namespace MediaBrowser.Model.Dto
         public string ParentLogoImageTag { get; set; }
         public string ParentLogoImageTag { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// If the item does not have a art, this will hold the Id of the Parent that has one.
+        /// Gets or sets wether the item has fan art, this will hold the Id of the Parent that has one.
         /// </summary>
         /// </summary>
         /// <value>The parent art item id.</value>
         /// <value>The parent art item id.</value>
         public string ParentArtItemId { get; set; }
         public string ParentArtItemId { get; set; }
@@ -695,7 +695,7 @@ namespace MediaBrowser.Model.Dto
         public string ChannelPrimaryImageTag { get; set; }
         public string ChannelPrimaryImageTag { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The start date of the recording, in UTC.
+        /// Gets or sets the start date of the recording, in UTC.
         /// </summary>
         /// </summary>
         public DateTime? StartDate { get; set; }
         public DateTime? StartDate { get; set; }
 
 

+ 34 - 33
MediaBrowser.Model/Dto/MediaSourceInfo.cs

@@ -12,6 +12,18 @@ namespace MediaBrowser.Model.Dto
 {
 {
     public class MediaSourceInfo
     public class MediaSourceInfo
     {
     {
+        public MediaSourceInfo()
+        {
+            Formats = Array.Empty<string>();
+            MediaStreams = new List<MediaStream>();
+            MediaAttachments = Array.Empty<MediaAttachment>();
+            RequiredHttpHeaders = new Dictionary<string, string>();
+            SupportsTranscoding = true;
+            SupportsDirectStream = true;
+            SupportsDirectPlay = true;
+            SupportsProbing = true;
+        }
+
         public MediaProtocol Protocol { get; set; }
         public MediaProtocol Protocol { get; set; }
 
 
         public string Id { get; set; }
         public string Id { get; set; }
@@ -31,6 +43,7 @@ namespace MediaBrowser.Model.Dto
         public string Name { get; set; }
         public string Name { get; set; }
 
 
         /// <summary>
         /// <summary>
+        /// Gets or sets a value indicating whether the media is remote.
         /// Differentiate internet url vs local network.
         /// Differentiate internet url vs local network.
         /// </summary>
         /// </summary>
         public bool IsRemote { get; set; }
         public bool IsRemote { get; set; }
@@ -95,16 +108,28 @@ namespace MediaBrowser.Model.Dto
 
 
         public int? AnalyzeDurationMs { get; set; }
         public int? AnalyzeDurationMs { get; set; }
 
 
-        public MediaSourceInfo()
+        [JsonIgnore]
+        public TranscodeReason[] TranscodeReasons { get; set; }
+
+        public int? DefaultAudioStreamIndex { get; set; }
+
+        public int? DefaultSubtitleStreamIndex { get; set; }
+
+        [JsonIgnore]
+        public MediaStream VideoStream
         {
         {
-            Formats = Array.Empty<string>();
-            MediaStreams = new List<MediaStream>();
-            MediaAttachments = Array.Empty<MediaAttachment>();
-            RequiredHttpHeaders = new Dictionary<string, string>();
-            SupportsTranscoding = true;
-            SupportsDirectStream = true;
-            SupportsDirectPlay = true;
-            SupportsProbing = true;
+            get
+            {
+                foreach (var i in MediaStreams)
+                {
+                    if (i.Type == MediaStreamType.Video)
+                    {
+                        return i;
+                    }
+                }
+
+                return null;
+            }
         }
         }
 
 
         public void InferTotalBitrate(bool force = false)
         public void InferTotalBitrate(bool force = false)
@@ -134,13 +159,6 @@ namespace MediaBrowser.Model.Dto
             }
             }
         }
         }
 
 
-        [JsonIgnore]
-        public TranscodeReason[] TranscodeReasons { get; set; }
-
-        public int? DefaultAudioStreamIndex { get; set; }
-
-        public int? DefaultSubtitleStreamIndex { get; set; }
-
         public MediaStream GetDefaultAudioStream(int? defaultIndex)
         public MediaStream GetDefaultAudioStream(int? defaultIndex)
         {
         {
             if (defaultIndex.HasValue)
             if (defaultIndex.HasValue)
@@ -175,23 +193,6 @@ namespace MediaBrowser.Model.Dto
             return null;
             return null;
         }
         }
 
 
-        [JsonIgnore]
-        public MediaStream VideoStream
-        {
-            get
-            {
-                foreach (var i in MediaStreams)
-                {
-                    if (i.Type == MediaStreamType.Video)
-                    {
-                        return i;
-                    }
-                }
-
-                return null;
-            }
-        }
-
         public MediaStream GetMediaStream(MediaStreamType type, int index)
         public MediaStream GetMediaStream(MediaStreamType type, int index)
         {
         {
             foreach (var i in MediaStreams)
             foreach (var i in MediaStreams)

+ 9 - 9
MediaBrowser.Model/Dto/MetadataEditorInfo.cs

@@ -10,6 +10,15 @@ namespace MediaBrowser.Model.Dto
 {
 {
     public class MetadataEditorInfo
     public class MetadataEditorInfo
     {
     {
+        public MetadataEditorInfo()
+        {
+            ParentalRatingOptions = Array.Empty<ParentalRating>();
+            Countries = Array.Empty<CountryInfo>();
+            Cultures = Array.Empty<CultureDto>();
+            ExternalIdInfos = Array.Empty<ExternalIdInfo>();
+            ContentTypeOptions = Array.Empty<NameValuePair>();
+        }
+
         public ParentalRating[] ParentalRatingOptions { get; set; }
         public ParentalRating[] ParentalRatingOptions { get; set; }
 
 
         public CountryInfo[] Countries { get; set; }
         public CountryInfo[] Countries { get; set; }
@@ -21,14 +30,5 @@ namespace MediaBrowser.Model.Dto
         public string ContentType { get; set; }
         public string ContentType { get; set; }
 
 
         public NameValuePair[] ContentTypeOptions { get; set; }
         public NameValuePair[] ContentTypeOptions { get; set; }
-
-        public MetadataEditorInfo()
-        {
-            ParentalRatingOptions = Array.Empty<ParentalRating>();
-            Countries = Array.Empty<CountryInfo>();
-            Cultures = Array.Empty<CultureDto>();
-            ExternalIdInfos = Array.Empty<ExternalIdInfo>();
-            ContentTypeOptions = Array.Empty<NameValuePair>();
-        }
     }
     }
 }
 }

+ 14 - 0
MediaBrowser.Model/Dto/NameGuidPair.cs

@@ -0,0 +1,14 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+    public class NameGuidPair
+    {
+        public string Name { get; set; }
+
+        public Guid Id { get; set; }
+    }
+}

+ 0 - 7
MediaBrowser.Model/Dto/NameIdPair.cs

@@ -19,11 +19,4 @@ namespace MediaBrowser.Model.Dto
         /// <value>The identifier.</value>
         /// <value>The identifier.</value>
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
-
-    public class NameGuidPair
-    {
-        public string Name { get; set; }
-
-        public Guid Id { get; set; }
-    }
 }
 }

+ 9 - 9
MediaBrowser.Model/Dto/UserDto.cs

@@ -10,6 +10,15 @@ namespace MediaBrowser.Model.Dto
     /// </summary>
     /// </summary>
     public class UserDto : IItemDto, IHasServerId
     public class UserDto : IItemDto, IHasServerId
     {
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserDto"/> class.
+        /// </summary>
+        public UserDto()
+        {
+            Configuration = new UserConfiguration();
+            Policy = new UserPolicy();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>
@@ -94,15 +103,6 @@ namespace MediaBrowser.Model.Dto
         /// <value>The primary image aspect ratio.</value>
         /// <value>The primary image aspect ratio.</value>
         public double? PrimaryImageAspectRatio { get; set; }
         public double? PrimaryImageAspectRatio { get; set; }
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="UserDto"/> class.
-        /// </summary>
-        public UserDto()
-        {
-            Configuration = new UserConfiguration();
-            Policy = new UserPolicy();
-        }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public override string ToString()
         public override string ToString()
         {
         {

+ 0 - 32
MediaBrowser.Model/Entities/CollectionType.cs

@@ -24,36 +24,4 @@ namespace MediaBrowser.Model.Entities
         public const string Playlists = "playlists";
         public const string Playlists = "playlists";
         public const string Folders = "folders";
         public const string Folders = "folders";
     }
     }
-
-    public static class SpecialFolder
-    {
-        public const string TvShowSeries = "TvShowSeries";
-        public const string TvGenres = "TvGenres";
-        public const string TvGenre = "TvGenre";
-        public const string TvLatest = "TvLatest";
-        public const string TvNextUp = "TvNextUp";
-        public const string TvResume = "TvResume";
-        public const string TvFavoriteSeries = "TvFavoriteSeries";
-        public const string TvFavoriteEpisodes = "TvFavoriteEpisodes";
-
-        public const string MovieLatest = "MovieLatest";
-        public const string MovieResume = "MovieResume";
-        public const string MovieMovies = "MovieMovies";
-        public const string MovieCollections = "MovieCollections";
-        public const string MovieFavorites = "MovieFavorites";
-        public const string MovieGenres = "MovieGenres";
-        public const string MovieGenre = "MovieGenre";
-
-        public const string MusicArtists = "MusicArtists";
-        public const string MusicAlbumArtists = "MusicAlbumArtists";
-        public const string MusicAlbums = "MusicAlbums";
-        public const string MusicGenres = "MusicGenres";
-        public const string MusicLatest = "MusicLatest";
-        public const string MusicPlaylists = "MusicPlaylists";
-        public const string MusicSongs = "MusicSongs";
-        public const string MusicFavorites = "MusicFavorites";
-        public const string MusicFavoriteArtists = "MusicFavoriteArtists";
-        public const string MusicFavoriteAlbums = "MusicFavoriteAlbums";
-        public const string MusicFavoriteSongs = "MusicFavoriteSongs";
-    }
 }
 }

+ 16 - 0
MediaBrowser.Model/Entities/CollectionTypeOptions.cs

@@ -0,0 +1,16 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Entities
+{
+    public enum CollectionTypeOptions
+    {
+        Movies = 0,
+        TvShows = 1,
+        Music = 2,
+        MusicVideos = 3,
+        HomeVideos = 4,
+        BoxSets = 5,
+        Books = 6,
+        Mixed = 7
+    }
+}

+ 98 - 99
MediaBrowser.Model/Entities/MediaStream.cs

@@ -84,7 +84,7 @@ namespace MediaBrowser.Model.Entities
         public string Title { get; set; }
         public string Title { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the video range.
+        /// Gets the video range.
         /// </summary>
         /// </summary>
         /// <value>The video range.</value>
         /// <value>The video range.</value>
         public string VideoRange
         public string VideoRange
@@ -108,11 +108,11 @@ namespace MediaBrowser.Model.Entities
             }
             }
         }
         }
 
 
-        public string localizedUndefined { get; set; }
+        public string LocalizedUndefined { get; set; }
 
 
-        public string localizedDefault { get; set; }
+        public string LocalizedDefault { get; set; }
 
 
-        public string localizedForced { get; set; }
+        public string LocalizedForced { get; set; }
 
 
         public string DisplayTitle
         public string DisplayTitle
         {
         {
@@ -154,7 +154,7 @@ namespace MediaBrowser.Model.Entities
 
 
                         if (IsDefault)
                         if (IsDefault)
                         {
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
                         }
                         }
 
 
                         if (!string.IsNullOrEmpty(Title))
                         if (!string.IsNullOrEmpty(Title))
@@ -229,17 +229,17 @@ namespace MediaBrowser.Model.Entities
                         }
                         }
                         else
                         else
                         {
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedUndefined) ? "Und" : LocalizedUndefined);
                         }
                         }
 
 
                         if (IsDefault)
                         if (IsDefault)
                         {
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
                         }
                         }
 
 
                         if (IsForced)
                         if (IsForced)
                         {
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced);
                         }
                         }
 
 
                         if (!string.IsNullOrEmpty(Title))
                         if (!string.IsNullOrEmpty(Title))
@@ -266,67 +266,6 @@ namespace MediaBrowser.Model.Entities
             }
             }
         }
         }
 
 
-        private string GetResolutionText()
-        {
-            var i = this;
-
-            if (i.Width.HasValue && i.Height.HasValue)
-            {
-                var width = i.Width.Value;
-                var height = i.Height.Value;
-
-                if (width >= 3800 || height >= 2000)
-                {
-                    return "4K";
-                }
-
-                if (width >= 2500)
-                {
-                    if (i.IsInterlaced)
-                    {
-                        return "1440i";
-                    }
-
-                    return "1440p";
-                }
-
-                if (width >= 1900 || height >= 1000)
-                {
-                    if (i.IsInterlaced)
-                    {
-                        return "1080i";
-                    }
-
-                    return "1080p";
-                }
-
-                if (width >= 1260 || height >= 700)
-                {
-                    if (i.IsInterlaced)
-                    {
-                        return "720i";
-                    }
-
-                    return "720p";
-                }
-
-                if (width >= 700 || height >= 440)
-                {
-
-                    if (i.IsInterlaced)
-                    {
-                        return "480i";
-                    }
-
-                    return "480p";
-                }
-
-                return "SD";
-            }
-
-            return null;
-        }
-
         public string NalLengthSize { get; set; }
         public string NalLengthSize { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -487,6 +426,96 @@ namespace MediaBrowser.Model.Entities
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether [supports external stream].
+        /// </summary>
+        /// <value><c>true</c> if [supports external stream]; otherwise, <c>false</c>.</value>
+        public bool SupportsExternalStream { get; set; }
+
+        /// <summary>
+        /// Gets or sets the filename.
+        /// </summary>
+        /// <value>The filename.</value>
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the pixel format.
+        /// </summary>
+        /// <value>The pixel format.</value>
+        public string PixelFormat { get; set; }
+
+        /// <summary>
+        /// Gets or sets the level.
+        /// </summary>
+        /// <value>The level.</value>
+        public double? Level { get; set; }
+
+        /// <summary>
+        /// Gets or sets whether this instance is anamorphic.
+        /// </summary>
+        /// <value><c>true</c> if this instance is anamorphic; otherwise, <c>false</c>.</value>
+        public bool? IsAnamorphic { get; set; }
+
+        private string GetResolutionText()
+        {
+            var i = this;
+
+            if (i.Width.HasValue && i.Height.HasValue)
+            {
+                var width = i.Width.Value;
+                var height = i.Height.Value;
+
+                if (width >= 3800 || height >= 2000)
+                {
+                    return "4K";
+                }
+
+                if (width >= 2500)
+                {
+                    if (i.IsInterlaced)
+                    {
+                        return "1440i";
+                    }
+
+                    return "1440p";
+                }
+
+                if (width >= 1900 || height >= 1000)
+                {
+                    if (i.IsInterlaced)
+                    {
+                        return "1080i";
+                    }
+
+                    return "1080p";
+                }
+
+                if (width >= 1260 || height >= 700)
+                {
+                    if (i.IsInterlaced)
+                    {
+                        return "720i";
+                    }
+
+                    return "720p";
+                }
+
+                if (width >= 700 || height >= 440)
+                {
+                    if (i.IsInterlaced)
+                    {
+                        return "480i";
+                    }
+
+                    return "480p";
+                }
+
+                return "SD";
+            }
+
+            return null;
+        }
+
         public static bool IsTextFormat(string format)
         public static bool IsTextFormat(string format)
         {
         {
             string codec = format ?? string.Empty;
             string codec = format ?? string.Empty;
@@ -533,35 +562,5 @@ namespace MediaBrowser.Model.Entities
 
 
             return true;
             return true;
         }
         }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [supports external stream].
-        /// </summary>
-        /// <value><c>true</c> if [supports external stream]; otherwise, <c>false</c>.</value>
-        public bool SupportsExternalStream { get; set; }
-
-        /// <summary>
-        /// Gets or sets the filename.
-        /// </summary>
-        /// <value>The filename.</value>
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets the pixel format.
-        /// </summary>
-        /// <value>The pixel format.</value>
-        public string PixelFormat { get; set; }
-
-        /// <summary>
-        /// Gets or sets the level.
-        /// </summary>
-        /// <value>The level.</value>
-        public double? Level { get; set; }
-
-        /// <summary>
-        /// Gets a value indicating whether this instance is anamorphic.
-        /// </summary>
-        /// <value><c>true</c> if this instance is anamorphic; otherwise, <c>false</c>.</value>
-        public bool? IsAnamorphic { get; set; }
     }
     }
 }
 }

+ 0 - 40
MediaBrowser.Model/Entities/PackageReviewInfo.cs

@@ -1,40 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Entities
-{
-    public class PackageReviewInfo
-    {
-        /// <summary>
-        /// Gets or sets the package id (database key) for this review.
-        /// </summary>
-        public int id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the rating value.
-        /// </summary>
-        public int rating { get; set; }
-
-        /// <summary>
-        /// Gets or sets whether or not this review recommends this item.
-        /// </summary>
-        public bool recommend { get; set; }
-
-        /// <summary>
-        /// Gets or sets a short description of the review.
-        /// </summary>
-        public string title { get; set; }
-
-        /// <summary>
-        /// Gets or sets the full review.
-        /// </summary>
-        public string review { get; set; }
-
-        /// <summary>
-        /// Gets or sets the time of review.
-        /// </summary>
-        public DateTime timestamp { get; set; }
-    }
-}

+ 35 - 26
MediaBrowser.Model/Entities/ProviderIdsExtensions.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 
 
 namespace MediaBrowser.Model.Entities
 namespace MediaBrowser.Model.Entities
 {
 {
@@ -9,14 +10,26 @@ namespace MediaBrowser.Model.Entities
     public static class ProviderIdsExtensions
     public static class ProviderIdsExtensions
     {
     {
         /// <summary>
         /// <summary>
-        /// Determines whether [has provider identifier] [the specified instance].
+        /// Gets a provider id.
         /// </summary>
         /// </summary>
         /// <param name="instance">The instance.</param>
         /// <param name="instance">The instance.</param>
-        /// <param name="provider">The provider.</param>
-        /// <returns><c>true</c> if [has provider identifier] [the specified instance]; otherwise, <c>false</c>.</returns>
-        public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
+        /// <param name="name">The name.</param>
+        /// <param name="id">The provider id.</param>
+        /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
+        public static bool TryGetProviderId(this IHasProviderIds instance, string name, [MaybeNullWhen(false)] out string id)
         {
         {
-            return !string.IsNullOrEmpty(instance.GetProviderId(provider.ToString()));
+            if (instance == null)
+            {
+                throw new ArgumentNullException(nameof(instance));
+            }
+
+            if (instance.ProviderIds == null)
+            {
+                id = null;
+                return false;
+            }
+
+            return instance.ProviderIds.TryGetValue(name, out id);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -24,10 +37,11 @@ namespace MediaBrowser.Model.Entities
         /// </summary>
         /// </summary>
         /// <param name="instance">The instance.</param>
         /// <param name="instance">The instance.</param>
         /// <param name="provider">The provider.</param>
         /// <param name="provider">The provider.</param>
-        /// <returns>System.String.</returns>
-        public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
+        /// <param name="id">The provider id.</param>
+        /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
+        public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [MaybeNullWhen(false)] out string id)
         {
         {
-            return instance.GetProviderId(provider.ToString());
+            return instance.TryGetProviderId(provider.ToString(), out id);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -38,18 +52,19 @@ namespace MediaBrowser.Model.Entities
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         public static string? GetProviderId(this IHasProviderIds instance, string name)
         public static string? GetProviderId(this IHasProviderIds instance, string name)
         {
         {
-            if (instance == null)
-            {
-                throw new ArgumentNullException(nameof(instance));
-            }
-
-            if (instance.ProviderIds == null)
-            {
-                return null;
-            }
+            instance.TryGetProviderId(name, out string? id);
+            return id;
+        }
 
 
-            instance.ProviderIds.TryGetValue(name, out string? id);
-            return string.IsNullOrEmpty(id) ? null : id;
+        /// <summary>
+        /// Gets a provider id.
+        /// </summary>
+        /// <param name="instance">The instance.</param>
+        /// <param name="provider">The provider.</param>
+        /// <returns>System.String.</returns>
+        public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
+        {
+            return instance.GetProviderId(provider.ToString());
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -68,13 +83,7 @@ namespace MediaBrowser.Model.Entities
             // If it's null remove the key from the dictionary
             // If it's null remove the key from the dictionary
             if (string.IsNullOrEmpty(value))
             if (string.IsNullOrEmpty(value))
             {
             {
-                if (instance.ProviderIds != null)
-                {
-                    if (instance.ProviderIds.ContainsKey(name))
-                    {
-                        instance.ProviderIds.Remove(name);
-                    }
-                }
+                instance.ProviderIds?.Remove(name);
             }
             }
             else
             else
             {
             {

+ 36 - 0
MediaBrowser.Model/Entities/SpecialFolder.cs

@@ -0,0 +1,36 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Entities
+{
+    public static class SpecialFolder
+    {
+        public const string TvShowSeries = "TvShowSeries";
+        public const string TvGenres = "TvGenres";
+        public const string TvGenre = "TvGenre";
+        public const string TvLatest = "TvLatest";
+        public const string TvNextUp = "TvNextUp";
+        public const string TvResume = "TvResume";
+        public const string TvFavoriteSeries = "TvFavoriteSeries";
+        public const string TvFavoriteEpisodes = "TvFavoriteEpisodes";
+
+        public const string MovieLatest = "MovieLatest";
+        public const string MovieResume = "MovieResume";
+        public const string MovieMovies = "MovieMovies";
+        public const string MovieCollections = "MovieCollections";
+        public const string MovieFavorites = "MovieFavorites";
+        public const string MovieGenres = "MovieGenres";
+        public const string MovieGenre = "MovieGenre";
+
+        public const string MusicArtists = "MusicArtists";
+        public const string MusicAlbumArtists = "MusicAlbumArtists";
+        public const string MusicAlbums = "MusicAlbums";
+        public const string MusicGenres = "MusicGenres";
+        public const string MusicLatest = "MusicLatest";
+        public const string MusicPlaylists = "MusicPlaylists";
+        public const string MusicSongs = "MusicSongs";
+        public const string MusicFavorites = "MusicFavorites";
+        public const string MusicFavoriteArtists = "MusicFavoriteArtists";
+        public const string MusicFavoriteAlbums = "MusicFavoriteAlbums";
+        public const string MusicFavoriteSongs = "MusicFavoriteSongs";
+    }
+}

+ 9 - 9
MediaBrowser.Model/Entities/VirtualFolderInfo.cs

@@ -11,6 +11,14 @@ namespace MediaBrowser.Model.Entities
     /// </summary>
     /// </summary>
     public class VirtualFolderInfo
     public class VirtualFolderInfo
     {
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
+        /// </summary>
+        public VirtualFolderInfo()
+        {
+            Locations = Array.Empty<string>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>
@@ -27,18 +35,10 @@ namespace MediaBrowser.Model.Entities
         /// Gets or sets the type of the collection.
         /// Gets or sets the type of the collection.
         /// </summary>
         /// </summary>
         /// <value>The type of the collection.</value>
         /// <value>The type of the collection.</value>
-        public string CollectionType { get; set; }
+        public CollectionTypeOptions? CollectionType { get; set; }
 
 
         public LibraryOptions LibraryOptions { get; set; }
         public LibraryOptions LibraryOptions { get; set; }
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
-        /// </summary>
-        public VirtualFolderInfo()
-        {
-            Locations = Array.Empty<string>();
-        }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the item identifier.
         /// Gets or sets the item identifier.
         /// </summary>
         /// </summary>

+ 6 - 6
MediaBrowser.Model/Globalization/CultureDto.cs

@@ -10,6 +10,11 @@ namespace MediaBrowser.Model.Globalization
     /// </summary>
     /// </summary>
     public class CultureDto
     public class CultureDto
     {
     {
+        public CultureDto()
+        {
+            ThreeLetterISOLanguageNames = Array.Empty<string>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>
@@ -29,7 +34,7 @@ namespace MediaBrowser.Model.Globalization
         public string TwoLetterISOLanguageName { get; set; }
         public string TwoLetterISOLanguageName { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the name of the three letter ISO language.
+        /// Gets the name of the three letter ISO language.
         /// </summary>
         /// </summary>
         /// <value>The name of the three letter ISO language.</value>
         /// <value>The name of the three letter ISO language.</value>
         public string ThreeLetterISOLanguageName
         public string ThreeLetterISOLanguageName
@@ -47,10 +52,5 @@ namespace MediaBrowser.Model.Globalization
         }
         }
 
 
         public string[] ThreeLetterISOLanguageNames { get; set; }
         public string[] ThreeLetterISOLanguageNames { get; set; }
-
-        public CultureDto()
-        {
-            ThreeLetterISOLanguageNames = Array.Empty<string>();
-        }
     }
     }
 }
 }

+ 5 - 2
MediaBrowser.Model/IO/IFileSystem.cs

@@ -155,13 +155,16 @@ namespace MediaBrowser.Model.IO
         /// Gets the directories.
         /// Gets the directories.
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
-        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
-        /// <returns>IEnumerable&lt;DirectoryInfo&gt;.</returns>
+        /// <param name="recursive">If set to <c>true</c> also searches in subdirectiories.</param>
+        /// <returns>All found directories.</returns>
         IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false);
         IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false);
 
 
         /// <summary>
         /// <summary>
         /// Gets the files.
         /// Gets the files.
         /// </summary>
         /// </summary>
+        /// <param name="path">The path in which to search.</param>
+        /// <param name="recursive">If set to <c>true</c> also searches in subdirectiories.</param>
+        /// <returns>All found files.</returns>
         IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false);
         IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false);
 
 
         IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive);
         IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive);

+ 8 - 8
MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs

@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.LiveTv
     public class BaseTimerInfoDto : IHasServerId
     public class BaseTimerInfoDto : IHasServerId
     {
     {
         /// <summary>
         /// <summary>
-        /// Id of the recording.
+        /// Gets or sets the Id of the recording.
         /// </summary>
         /// </summary>
         public string Id { get; set; }
         public string Id { get; set; }
 
 
@@ -28,7 +28,7 @@ namespace MediaBrowser.Model.LiveTv
         public string ExternalId { get; set; }
         public string ExternalId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// ChannelId of the recording.
+        /// Gets or sets the channel id of the recording.
         /// </summary>
         /// </summary>
         public Guid ChannelId { get; set; }
         public Guid ChannelId { get; set; }
 
 
@@ -39,7 +39,7 @@ namespace MediaBrowser.Model.LiveTv
         public string ExternalChannelId { get; set; }
         public string ExternalChannelId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// ChannelName of the recording.
+        /// Gets or sets the channel name of the recording.
         /// </summary>
         /// </summary>
         public string ChannelName { get; set; }
         public string ChannelName { get; set; }
 
 
@@ -58,22 +58,22 @@ namespace MediaBrowser.Model.LiveTv
         public string ExternalProgramId { get; set; }
         public string ExternalProgramId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Name of the recording.
+        /// Gets or sets the name of the recording.
         /// </summary>
         /// </summary>
         public string Name { get; set; }
         public string Name { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Description of the recording.
+        /// Gets or sets the description of the recording.
         /// </summary>
         /// </summary>
         public string Overview { get; set; }
         public string Overview { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The start date of the recording, in UTC.
+        /// Gets or sets the start date of the recording, in UTC.
         /// </summary>
         /// </summary>
         public DateTime StartDate { get; set; }
         public DateTime StartDate { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The end date of the recording, in UTC.
+        /// Gets or sets the end date of the recording, in UTC.
         /// </summary>
         /// </summary>
         public DateTime EndDate { get; set; }
         public DateTime EndDate { get; set; }
 
 
@@ -108,7 +108,7 @@ namespace MediaBrowser.Model.LiveTv
         public bool IsPrePaddingRequired { get; set; }
         public bool IsPrePaddingRequired { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+        /// Gets or sets the Id of the Parent that has a backdrop if the item does not have one.
         /// </summary>
         /// </summary>
         /// <value>The parent backdrop item id.</value>
         /// <value>The parent backdrop item id.</value>
         public string ParentBackdropItemId { get; set; }
         public string ParentBackdropItemId { get; set; }

+ 58 - 0
MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs

@@ -0,0 +1,58 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Model.LiveTv
+{
+    public class ListingsProviderInfo
+    {
+        public ListingsProviderInfo()
+        {
+            NewsCategories = new[] { "news", "journalism", "documentary", "current affairs" };
+            SportsCategories = new[] { "sports", "basketball", "baseball", "football" };
+            KidsCategories = new[] { "kids", "family", "children", "childrens", "disney" };
+            MovieCategories = new[] { "movie" };
+            EnabledTuners = Array.Empty<string>();
+            EnableAllTuners = true;
+            ChannelMappings = Array.Empty<NameValuePair>();
+        }
+
+        public string Id { get; set; }
+
+        public string Type { get; set; }
+
+        public string Username { get; set; }
+
+        public string Password { get; set; }
+
+        public string ListingsId { get; set; }
+
+        public string ZipCode { get; set; }
+
+        public string Country { get; set; }
+
+        public string Path { get; set; }
+
+        public string[] EnabledTuners { get; set; }
+
+        public bool EnableAllTuners { get; set; }
+
+        public string[] NewsCategories { get; set; }
+
+        public string[] SportsCategories { get; set; }
+
+        public string[] KidsCategories { get; set; }
+
+        public string[] MovieCategories { get; set; }
+
+        public NameValuePair[] ChannelMappings { get; set; }
+
+        public string MoviePrefix { get; set; }
+
+        public string PreferredLanguage { get; set; }
+
+        public string UserAgent { get; set; }
+    }
+}

+ 13 - 13
MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs

@@ -11,6 +11,12 @@ namespace MediaBrowser.Model.LiveTv
     /// </summary>
     /// </summary>
     public class LiveTvChannelQuery
     public class LiveTvChannelQuery
     {
     {
+        public LiveTvChannelQuery()
+        {
+            EnableUserData = true;
+            SortBy = Array.Empty<string>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the type of the channel.
         /// Gets or sets the type of the channel.
         /// </summary>
         /// </summary>
@@ -48,13 +54,13 @@ namespace MediaBrowser.Model.LiveTv
         public Guid UserId { get; set; }
         public Guid UserId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Used for paging.
         /// </summary>
         /// </summary>
         /// <value>The start index.</value>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
         public int? StartIndex { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// </summary>
         /// <value>The limit.</value>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
         public int? Limit { get; set; }
@@ -68,15 +74,15 @@ namespace MediaBrowser.Model.LiveTv
         public bool EnableUserData { get; set; }
         public bool EnableUserData { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Used to specific whether to return news or not.
+        /// Gets or sets a value whether to return news or not.
         /// </summary>
         /// </summary>
-        /// <remarks>If set to null, all programs will be returned</remarks>
+        /// <remarks>If set to <c>null</c>, all programs will be returned.</remarks>
         public bool? IsNews { get; set; }
         public bool? IsNews { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Used to specific whether to return movies or not.
+        /// Gets or sets a value whether to return movies or not.
         /// </summary>
         /// </summary>
-        /// <remarks>If set to null, all programs will be returned</remarks>
+        /// <remarks>If set to <c>null</c>, all programs will be returned.</remarks>
         public bool? IsMovie { get; set; }
         public bool? IsMovie { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -96,15 +102,9 @@ namespace MediaBrowser.Model.LiveTv
         public string[] SortBy { get; set; }
         public string[] SortBy { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The sort order to return results with.
+        /// Gets or sets the sort order to return results with.
         /// </summary>
         /// </summary>
         /// <value>The sort order.</value>
         /// <value>The sort order.</value>
         public SortOrder? SortOrder { get; set; }
         public SortOrder? SortOrder { get; set; }
-
-        public LiveTvChannelQuery()
-        {
-            EnableUserData = true;
-            SortBy = Array.Empty<string>();
-        }
     }
     }
 }
 }

+ 8 - 89
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -2,12 +2,19 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using MediaBrowser.Model.Dto;
 
 
 namespace MediaBrowser.Model.LiveTv
 namespace MediaBrowser.Model.LiveTv
 {
 {
     public class LiveTvOptions
     public class LiveTvOptions
     {
     {
+        public LiveTvOptions()
+        {
+            TunerHosts = Array.Empty<TunerHostInfo>();
+            ListingProviders = Array.Empty<ListingsProviderInfo>();
+            MediaLocationsCreated = Array.Empty<string>();
+            RecordingPostProcessorArguments = "\"{path}\"";
+        }
+
         public int? GuideDays { get; set; }
         public int? GuideDays { get; set; }
 
 
         public string RecordingPath { get; set; }
         public string RecordingPath { get; set; }
@@ -33,93 +40,5 @@ namespace MediaBrowser.Model.LiveTv
         public string RecordingPostProcessor { get; set; }
         public string RecordingPostProcessor { get; set; }
 
 
         public string RecordingPostProcessorArguments { get; set; }
         public string RecordingPostProcessorArguments { get; set; }
-
-        public LiveTvOptions()
-        {
-            TunerHosts = Array.Empty<TunerHostInfo>();
-            ListingProviders = Array.Empty<ListingsProviderInfo>();
-            MediaLocationsCreated = Array.Empty<string>();
-            RecordingPostProcessorArguments = "\"{path}\"";
-        }
-    }
-
-    public class TunerHostInfo
-    {
-        public string Id { get; set; }
-
-        public string Url { get; set; }
-
-        public string Type { get; set; }
-
-        public string DeviceId { get; set; }
-
-        public string FriendlyName { get; set; }
-
-        public bool ImportFavoritesOnly { get; set; }
-
-        public bool AllowHWTranscoding { get; set; }
-
-        public bool EnableStreamLooping { get; set; }
-
-        public string Source { get; set; }
-
-        public int TunerCount { get; set; }
-
-        public string UserAgent { get; set; }
-
-        public TunerHostInfo()
-        {
-            AllowHWTranscoding = true;
-        }
-    }
-
-    public class ListingsProviderInfo
-    {
-        public string Id { get; set; }
-
-        public string Type { get; set; }
-
-        public string Username { get; set; }
-
-        public string Password { get; set; }
-
-        public string ListingsId { get; set; }
-
-        public string ZipCode { get; set; }
-
-        public string Country { get; set; }
-
-        public string Path { get; set; }
-
-        public string[] EnabledTuners { get; set; }
-
-        public bool EnableAllTuners { get; set; }
-
-        public string[] NewsCategories { get; set; }
-
-        public string[] SportsCategories { get; set; }
-
-        public string[] KidsCategories { get; set; }
-
-        public string[] MovieCategories { get; set; }
-
-        public NameValuePair[] ChannelMappings { get; set; }
-
-        public string MoviePrefix { get; set; }
-
-        public string PreferredLanguage { get; set; }
-
-        public string UserAgent { get; set; }
-
-        public ListingsProviderInfo()
-        {
-            NewsCategories = new[] { "news", "journalism", "documentary", "current affairs" };
-            SportsCategories = new[] { "sports", "basketball", "baseball", "football" };
-            KidsCategories = new[] { "kids", "family", "children", "childrens", "disney" };
-            MovieCategories = new[] { "movie" };
-            EnabledTuners = Array.Empty<string>();
-            EnableAllTuners = true;
-            ChannelMappings = Array.Empty<NameValuePair>();
-        }
     }
     }
 }
 }

+ 5 - 5
MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs

@@ -10,6 +10,11 @@ namespace MediaBrowser.Model.LiveTv
     /// </summary>
     /// </summary>
     public class LiveTvServiceInfo
     public class LiveTvServiceInfo
     {
     {
+        public LiveTvServiceInfo()
+        {
+            Tuners = Array.Empty<string>();
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name.
         /// Gets or sets the name.
         /// </summary>
         /// </summary>
@@ -53,10 +58,5 @@ namespace MediaBrowser.Model.LiveTv
         public bool IsVisible { get; set; }
         public bool IsVisible { get; set; }
 
 
         public string[] Tuners { get; set; }
         public string[] Tuners { get; set; }
-
-        public LiveTvServiceInfo()
-        {
-            Tuners = Array.Empty<string>();
-        }
     }
     }
 }
 }

+ 8 - 8
MediaBrowser.Model/LiveTv/RecordingQuery.cs

@@ -12,6 +12,11 @@ namespace MediaBrowser.Model.LiveTv
     /// </summary>
     /// </summary>
     public class RecordingQuery
     public class RecordingQuery
     {
     {
+        public RecordingQuery()
+        {
+            EnableTotalRecordCount = true;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the channel identifier.
         /// Gets or sets the channel identifier.
         /// </summary>
         /// </summary>
@@ -31,13 +36,13 @@ namespace MediaBrowser.Model.LiveTv
         public string Id { get; set; }
         public string Id { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Use for paging.
         /// </summary>
         /// </summary>
         /// <value>The start index.</value>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
         public int? StartIndex { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// </summary>
         /// <value>The limit.</value>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
         public int? Limit { get; set; }
@@ -61,7 +66,7 @@ namespace MediaBrowser.Model.LiveTv
         public string SeriesTimerId { get; set; }
         public string SeriesTimerId { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Fields to return within the items, in addition to basic information.
+        /// Gets or sets the fields to return within the items, in addition to basic information.
         /// </summary>
         /// </summary>
         /// <value>The fields.</value>
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }
         public ItemFields[] Fields { get; set; }
@@ -85,10 +90,5 @@ namespace MediaBrowser.Model.LiveTv
         public ImageType[] EnableImageTypes { get; set; }
         public ImageType[] EnableImageTypes { get; set; }
 
 
         public bool EnableTotalRecordCount { get; set; }
         public bool EnableTotalRecordCount { get; set; }
-
-        public RecordingQuery()
-        {
-            EnableTotalRecordCount = true;
-        }
     }
     }
 }
 }

+ 8 - 8
MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs

@@ -7,6 +7,14 @@ using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Model.LiveTv
 namespace MediaBrowser.Model.LiveTv
 {
 {
+    public enum KeepUntil
+    {
+        UntilDeleted,
+        UntilSpaceNeeded,
+        UntilWatched,
+        UntilDate
+    }
+
     /// <summary>
     /// <summary>
     /// Class SeriesTimerInfoDto.
     /// Class SeriesTimerInfoDto.
     /// </summary>
     /// </summary>
@@ -83,12 +91,4 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The parent primary image tag.</value>
         /// <value>The parent primary image tag.</value>
         public string ParentPrimaryImageTag { get; set; }
         public string ParentPrimaryImageTag { get; set; }
     }
     }
-
-    public enum KeepUntil
-    {
-        UntilDeleted,
-        UntilSpaceNeeded,
-        UntilWatched,
-        UntilDate
-    }
 }
 }

+ 38 - 0
MediaBrowser.Model/LiveTv/TunerHostInfo.cs

@@ -0,0 +1,38 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Model.LiveTv
+{
+    public class TunerHostInfo
+    {
+        public TunerHostInfo()
+        {
+            AllowHWTranscoding = true;
+        }
+
+        public string Id { get; set; }
+
+        public string Url { get; set; }
+
+        public string Type { get; set; }
+
+        public string DeviceId { get; set; }
+
+        public string FriendlyName { get; set; }
+
+        public bool ImportFavoritesOnly { get; set; }
+
+        public bool AllowHWTranscoding { get; set; }
+
+        public bool EnableStreamLooping { get; set; }
+
+        public string Source { get; set; }
+
+        public int TunerCount { get; set; }
+
+        public string UserAgent { get; set; }
+    }
+}

+ 2 - 2
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -17,7 +17,7 @@
     <TargetFramework>net5.0</TargetFramework>
     <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -44,7 +44,7 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

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

@@ -10,6 +10,17 @@ namespace MediaBrowser.Model.MediaInfo
 {
 {
     public class MediaInfo : MediaSourceInfo, IHasProviderIds
     public class MediaInfo : MediaSourceInfo, IHasProviderIds
     {
     {
+        public MediaInfo()
+        {
+            Chapters = Array.Empty<ChapterInfo>();
+            Artists = Array.Empty<string>();
+            AlbumArtists = Array.Empty<string>();
+            Studios = Array.Empty<string>();
+            Genres = Array.Empty<string>();
+            People = Array.Empty<BaseItemPerson>();
+            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+
         public ChapterInfo[] Chapters { get; set; }
         public ChapterInfo[] Chapters { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -69,16 +80,5 @@ namespace MediaBrowser.Model.MediaInfo
         /// </summary>
         /// </summary>
         /// <value>The overview.</value>
         /// <value>The overview.</value>
         public string Overview { get; set; }
         public string Overview { get; set; }
-
-        public MediaInfo()
-        {
-            Chapters = Array.Empty<ChapterInfo>();
-            Artists = Array.Empty<string>();
-            AlbumArtists = Array.Empty<string>();
-            Studios = Array.Empty<string>();
-            Genres = Array.Empty<string>();
-            People = Array.Empty<BaseItemPerson>();
-            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        }
     }
     }
 }
 }

+ 11 - 11
MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs

@@ -8,6 +8,17 @@ namespace MediaBrowser.Model.MediaInfo
 {
 {
     public class PlaybackInfoRequest
     public class PlaybackInfoRequest
     {
     {
+        public PlaybackInfoRequest()
+        {
+            EnableDirectPlay = true;
+            EnableDirectStream = true;
+            EnableTranscoding = true;
+            AllowVideoStreamCopy = true;
+            AllowAudioStreamCopy = true;
+            IsPlayback = true;
+            DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
+        }
+
         public Guid Id { get; set; }
         public Guid Id { get; set; }
 
 
         public Guid UserId { get; set; }
         public Guid UserId { get; set; }
@@ -43,16 +54,5 @@ namespace MediaBrowser.Model.MediaInfo
         public bool AutoOpenLiveStream { get; set; }
         public bool AutoOpenLiveStream { get; set; }
 
 
         public MediaProtocol[] DirectPlayProtocols { get; set; }
         public MediaProtocol[] DirectPlayProtocols { get; set; }
-
-        public PlaybackInfoRequest()
-        {
-            EnableDirectPlay = true;
-            EnableDirectStream = true;
-            EnableTranscoding = true;
-            AllowVideoStreamCopy = true;
-            AllowAudioStreamCopy = true;
-            IsPlayback = true;
-            DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
-        }
     }
     }
 }
 }

Some files were not shown because too many files changed in this diff