Ver Fonte

Merge branch 'master' into tests11

Bond-009 há 4 anos atrás
pai
commit
e09e67deae
100 ficheiros alterados com 1868 adições e 2494 exclusões
  1. 0 1
      Emby.Dlna/ContentDirectory/StubType.cs
  2. 1 1
      Emby.Dlna/DlnaManager.cs
  3. 0 1
      Emby.Dlna/PlayTo/TransportState.cs
  4. 1 1
      Emby.Server.Implementations/ApplicationHost.cs
  5. 3 3
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  6. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  7. 4 3
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  8. 0 1
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  9. 0 5
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  10. 0 1
      Emby.Server.Implementations/Session/WebSocketController.cs
  11. 11 32
      Jellyfin.Api/Controllers/DashboardController.cs
  12. 2 2
      Jellyfin.Api/Controllers/ImageController.cs
  13. 0 9
      Jellyfin.Api/Extensions/DtoExtensions.cs
  14. 7 4
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  15. 1 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  16. 2 2
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  17. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  18. 8 1
      Jellyfin.Api/Models/ConfigurationPageInfo.cs
  19. 1 1
      Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
  20. 0 1
      Jellyfin.Server/CoreAppHost.cs
  21. 1 1
      Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
  22. 51 0
      MediaBrowser.Common/Extensions/StreamExtensions.cs
  23. 0 37
      MediaBrowser.Common/Extensions/StringExtensions.cs
  24. 0 5
      MediaBrowser.Common/Net/NetworkExtensions.cs
  25. 0 3
      MediaBrowser.Common/Plugins/BasePlugin.cs
  26. 3 3
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  27. 1 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  28. 3 3
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  29. 12 121
      MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
  30. 11 92
      MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
  31. 11 467
      MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
  32. 63 0
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
  33. 2 5
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  34. 9 9
      MediaBrowser.Model/Channels/ChannelFeatures.cs
  35. 3 3
      MediaBrowser.Model/Channels/ChannelQuery.cs
  36. 37 37
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  37. 5 5
      MediaBrowser.Model/Configuration/ImageOption.cs
  38. 19 384
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  39. 12 0
      MediaBrowser.Model/Configuration/MediaPathInfo.cs
  40. 2 2
      MediaBrowser.Model/Configuration/MetadataConfiguration.cs
  41. 10 10
      MediaBrowser.Model/Configuration/MetadataOptions.cs
  42. 6 6
      MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
  43. 365 0
      MediaBrowser.Model/Configuration/TypeOptions.cs
  44. 18 18
      MediaBrowser.Model/Configuration/UserConfiguration.cs
  45. 8 8
      MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
  46. 5 4
      MediaBrowser.Model/Dlna/AudioOptions.cs
  47. 6 6
      MediaBrowser.Model/Dlna/CodecProfile.cs
  48. 1 1
      MediaBrowser.Model/Dlna/ConditionProcessor.cs
  49. 5 5
      MediaBrowser.Model/Dlna/ContainerProfile.cs
  50. 19 17
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  51. 1 0
      MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
  52. 2 2
      MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
  53. 4 4
      MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
  54. 5 5
      MediaBrowser.Model/Dlna/ResponseProfile.cs
  55. 27 27
      MediaBrowser.Model/Dlna/SearchCriteria.cs
  56. 2 2
      MediaBrowser.Model/Dlna/SortCriteria.cs
  57. 8 6
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  58. 577 597
      MediaBrowser.Model/Dlna/StreamInfo.cs
  59. 5 5
      MediaBrowser.Model/Dto/BaseItemDto.cs
  60. 34 33
      MediaBrowser.Model/Dto/MediaSourceInfo.cs
  61. 9 9
      MediaBrowser.Model/Dto/MetadataEditorInfo.cs
  62. 14 0
      MediaBrowser.Model/Dto/NameGuidPair.cs
  63. 0 7
      MediaBrowser.Model/Dto/NameIdPair.cs
  64. 9 9
      MediaBrowser.Model/Dto/UserDto.cs
  65. 0 32
      MediaBrowser.Model/Entities/CollectionType.cs
  66. 98 99
      MediaBrowser.Model/Entities/MediaStream.cs
  67. 0 40
      MediaBrowser.Model/Entities/PackageReviewInfo.cs
  68. 35 26
      MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
  69. 36 0
      MediaBrowser.Model/Entities/SpecialFolder.cs
  70. 8 8
      MediaBrowser.Model/Entities/VirtualFolderInfo.cs
  71. 6 6
      MediaBrowser.Model/Globalization/CultureDto.cs
  72. 5 2
      MediaBrowser.Model/IO/IFileSystem.cs
  73. 8 8
      MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
  74. 58 0
      MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs
  75. 13 13
      MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
  76. 8 89
      MediaBrowser.Model/LiveTv/LiveTvOptions.cs
  77. 5 5
      MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
  78. 8 8
      MediaBrowser.Model/LiveTv/RecordingQuery.cs
  79. 8 8
      MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
  80. 38 0
      MediaBrowser.Model/LiveTv/TunerHostInfo.cs
  81. 2 2
      MediaBrowser.Model/MediaBrowser.Model.csproj
  82. 11 11
      MediaBrowser.Model/MediaInfo/MediaInfo.cs
  83. 11 11
      MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
  84. 8 8
      MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
  85. 6 1
      MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs
  86. 3 2
      MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
  87. 6 0
      MediaBrowser.Model/Net/ISocket.cs
  88. 3 0
      MediaBrowser.Model/Net/ISocketFactory.cs
  89. 12 9
      MediaBrowser.Model/Net/MimeTypes.cs
  90. 0 33
      MediaBrowser.Model/Net/NetworkShare.cs
  91. 2 2
      MediaBrowser.Model/Net/SocketReceiveResult.cs
  92. 1 1
      MediaBrowser.Model/Net/WebSocketMessage.cs
  93. 5 5
      MediaBrowser.Model/Notifications/NotificationOptions.cs
  94. 7 7
      MediaBrowser.Model/Notifications/NotificationRequest.cs
  95. 2 2
      MediaBrowser.Model/Providers/ExternalIdInfo.cs
  96. 1 1
      MediaBrowser.Model/Providers/RemoteImageInfo.cs
  97. 8 8
      MediaBrowser.Model/Providers/SubtitleOptions.cs
  98. 5 5
      MediaBrowser.Model/Querying/EpisodeQuery.cs
  99. 5 4
      MediaBrowser.Model/Querying/LatestItemsQuery.cs
  100. 7 7
      MediaBrowser.Model/Querying/MovieRecommendationQuery.cs

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

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

+ 1 - 1
Emby.Dlna/DlnaManager.cs

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

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

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

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

@@ -374,7 +374,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Creates an instance of type and resolves all constructor dependencies.
         /// </summary>
-        /// /// <typeparam name="T">The type.</typeparam>
+        /// <typeparam name="T">The type.</typeparam>
         /// <returns>T.</returns>
         public T CreateInstance<T>()
             => ActivatorUtilities.CreateInstance<T>(ServiceProvider);

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

@@ -6207,9 +6207,9 @@ AND Type = @InternalPersonType)");
 
             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;

+ 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="Mono.Nat" Version="3.0.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="DotNet.Glob" Version="3.1.2" />
   </ItemGroup>

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

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

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

@@ -10,7 +10,6 @@ using System.Net.Http;
 using System.Net.Mime;
 using System.Text;
 using System.Text.Json;
-using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
 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('/');
         }
 
-        protected EncodingOptions GetEncodingOptions()
-        {
-            return Config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
         private static string GetHdHrIdFromChannelId(string channelId)
         {
             return channelId.Split('_')[1];

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

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

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

@@ -6,7 +6,6 @@ using System.Net.Mime;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Models;
 using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Http;
@@ -22,22 +21,18 @@ namespace Jellyfin.Api.Controllers
     public class DashboardController : BaseJellyfinApiController
     {
         private readonly ILogger<DashboardController> _logger;
-        private readonly IServerApplicationHost _appHost;
         private readonly IPluginManager _pluginManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// </summary>
         /// <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>
         public DashboardController(
             ILogger<DashboardController> logger,
-            IServerApplicationHost appHost,
             IPluginManager pluginManager)
         {
             _logger = logger;
-            _appHost = appHost;
             _pluginManager = pluginManager;
         }
 
@@ -51,7 +46,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("web/ConfigurationPages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
+        public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
             [FromQuery] bool? enableInMainMenu)
         {
             var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
@@ -77,38 +72,22 @@ namespace Jellyfin.Api.Controllers
         [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
         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));
-            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)
@@ -120,7 +99,7 @@ namespace Jellyfin.Api.Controllers
         {
             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));

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

@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
                 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
                 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
                 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
                 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)

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

@@ -113,14 +113,5 @@ namespace Jellyfin.Api.Extensions
 
             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.Linq;
 using System.Net;
+using System.Net.Mime;
 using System.Security.Claims;
 using System.Text;
 using System.Threading;
@@ -171,13 +172,15 @@ namespace Jellyfin.Api.Helpers
             var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
 
             // 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;
             }
 
             // 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;
             }
@@ -560,13 +563,13 @@ namespace Jellyfin.Api.Helpers
                 profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
                 if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
                 {
-                    profileString = profileString ?? "high";
+                    profileString ??= "high";
                 }
 
                 if (string.Equals(state.ActualOutputVideoCodec, "h265", 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>
         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)

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

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

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

@@ -17,8 +17,8 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
     <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>

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

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

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

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

+ 0 - 1
Jellyfin.Server/CoreAppHost.cs

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

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

@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Migrations.Routines
         public void Perform()
         {
             // 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)
             {
                 _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];
-        }
-    }
-}

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

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

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

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

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

@@ -103,7 +103,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         public void SetFFmpegPath()
         {
             // 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
                 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
-            var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
+            var config = _configurationManager.GetEncodingOptions();
             config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
             _configurationManager.SaveConfiguration("encoding", config);
 
@@ -177,7 +177,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             // Write the new ffmpeg path to the xml as <EncoderAppPath>
             // This ensures its not lost on next startup
-            var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
+            var config = _configurationManager.GetEncodingOptions();
             config.EncoderAppPath = newPath;
             _configurationManager.SaveConfiguration("encoding", config);
 

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

@@ -24,6 +24,7 @@
 
   <ItemGroup>
     <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="System.Text.Encoding.CodePages" Version="5.0.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.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))
             {

+ 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
 {
-    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 Nikse.SubtitleEdit.Core.SubtitleFormats;
 
 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
 {
     /// <summary>
-    /// <see href="https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs">Credit</see>.
+    /// SubStation Alpha subtitle parser.
     /// </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;
+        }
+    }
+}

+ 2 - 5
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -27,7 +27,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 {
     public class SubtitleEncoder : ISubtitleEncoder
     {
-        private readonly ILibraryManager _libraryManager;
         private readonly ILogger<SubtitleEncoder> _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
@@ -42,7 +41,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             new ConcurrentDictionary<string, SemaphoreSlim>();
 
         public SubtitleEncoder(
-            ILibraryManager libraryManager,
             ILogger<SubtitleEncoder> logger,
             IApplicationPaths appPaths,
             IFileSystem fileSystem,
@@ -50,7 +48,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             IHttpClientFactory httpClientFactory,
             IMediaSourceManager mediaSourceManager)
         {
-            _libraryManager = libraryManager;
             _logger = logger;
             _appPaths = appPaths;
             _fileSystem = fileSystem;
@@ -279,12 +276,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
             if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
             {
-                return new SsaParser();
+                return new SsaParser(_logger);
             }
 
             if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
             {
-                return new AssParser();
+                return new AssParser(_logger);
             }
 
             if (throwIfMissing)

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

@@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Channels
 {
     public class ChannelFeatures
     {
+        public ChannelFeatures()
+        {
+            MediaTypes = Array.Empty<ChannelMediaType>();
+            ContentTypes = Array.Empty<ChannelMediaContentType>();
+            DefaultSortFields = Array.Empty<ChannelItemSortField>();
+        }
+
         /// <summary>
         /// Gets or sets the name.
         /// </summary>
@@ -38,7 +45,7 @@ namespace MediaBrowser.Model.Channels
         public ChannelMediaContentType[] ContentTypes { get; set; }
 
         /// <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>
         public int? MaxPageSize { get; set; }
 
@@ -55,7 +62,7 @@ namespace MediaBrowser.Model.Channels
         public ChannelItemSortField[] DefaultSortFields { get; set; }
 
         /// <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>
         public bool SupportsSortOrderToggle { get; set; }
 
@@ -76,12 +83,5 @@ namespace MediaBrowser.Model.Channels
         /// </summary>
         /// <value><c>true</c> if [supports content downloading]; otherwise, <c>false</c>.</value>
         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
     {
         /// <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>
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }
@@ -28,13 +28,13 @@ namespace MediaBrowser.Model.Channels
         public Guid UserId { get; set; }
 
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Use for paging.
         /// </summary>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
 
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// <value>The limit.</value>
         public int? Limit { get; set; }

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

@@ -5,6 +5,41 @@ namespace MediaBrowser.Model.Configuration
 {
     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 string TranscodingTempPath { get; set; }
@@ -24,12 +59,12 @@ namespace MediaBrowser.Model.Configuration
         public string HardwareAccelerationType { get; set; }
 
         /// <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>
         public string EncoderAppPath { get; set; }
 
         /// <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>
         public string EncoderAppPathDisplay { get; set; }
 
@@ -76,40 +111,5 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableSubtitleExtraction { 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 ImageOption()
+        {
+            Limit = 1;
+        }
+
         /// <summary>
         /// Gets or sets the type.
         /// </summary>
@@ -23,10 +28,5 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// <value>The minimum width.</value>
         public int MinWidth { get; set; }
-
-        public ImageOption()
-        {
-            Limit = 1;
-        }
     }
 }

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

@@ -2,13 +2,30 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Model.Configuration
 {
     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 EnableRealtimeMonitor { get; set; }
@@ -79,387 +96,5 @@ namespace MediaBrowser.Model.Configuration
 
             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 bool UseFileCreationTimeForDateAdded { get; set; }
-
         public MetadataConfiguration()
         {
             UseFileCreationTimeForDateAdded = true;
         }
+
+        public bool UseFileCreationTimeForDateAdded { get; set; }
     }
 }

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

@@ -10,6 +10,16 @@ namespace MediaBrowser.Model.Configuration
     /// </summary>
     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[] DisabledMetadataSavers { get; set; }
@@ -23,15 +33,5 @@ namespace MediaBrowser.Model.Configuration
         public string[] DisabledImageFetchers { 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 MetadataPluginSummary()
+        {
+            SupportedImageTypes = Array.Empty<ImageType>();
+            Plugins = Array.Empty<MetadataPlugin>();
+        }
+
         /// <summary>
         /// Gets or sets the type of the item.
         /// </summary>
@@ -25,11 +31,5 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// <value>The supported image types.</value>
         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>
     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>
         /// Gets or sets the audio language preference.
         /// </summary>
@@ -52,23 +70,5 @@ namespace MediaBrowser.Model.Configuration
         public bool RememberSubtitleSelections { 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 XbmcMetadataOptions()
+        {
+            ReleaseDateFormat = "yyyy-MM-dd";
+
+            SaveImagePathsInNfo = true;
+            EnablePathSubstitution = true;
+        }
+
         public string UserId { get; set; }
 
         public string ReleaseDateFormat { get; set; }
@@ -14,13 +22,5 @@ namespace MediaBrowser.Model.Configuration
         public bool EnablePathSubstitution { 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; }
 
         /// <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>
         public string MediaSourceId { get; set; }
 
         public string DeviceId { get; set; }
 
         /// <summary>
-        /// Allows an override of supported number of audio channels
-        /// Example: DeviceProfile supports five channel, but user only has stereo speakers
+        /// Gets or sets an override of supported number of audio channels
+        /// Example: DeviceProfile supports five channel, but user only has stereo speakers.
         /// </summary>
         public int? MaxAudioChannels { get; set; }
 
         /// <summary>
-        /// The application's configured quality setting.
+        /// Gets or sets the application's configured quality setting.
         /// </summary>
         public int? MaxBitrate { get; set; }
 
@@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Dlna
         /// <summary>
         /// Gets the maximum bitrate.
         /// </summary>
+        /// <param name="isAudio">Whether or not this is audio.</param>
         /// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
         public int? GetMaxBitrate(bool isAudio)
         {

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

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

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

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

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

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

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

@@ -81,13 +81,13 @@ namespace MediaBrowser.Model.Dlna
                             DlnaFlags.DlnaV15;
 
             // 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(
                 CultureInfo.InvariantCulture,
@@ -150,16 +150,18 @@ namespace MediaBrowser.Model.Dlna
                             DlnaFlags.DlnaV15;
 
             // 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(
                 container,

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

@@ -8,6 +8,7 @@ namespace MediaBrowser.Model.Dlna
     public interface IDeviceDiscovery
     {
         event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
+
         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, "m2ts", StringComparison.OrdinalIgnoreCase))
             {
-
                 return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
             }
 
@@ -323,7 +322,6 @@ namespace MediaBrowser.Model.Dlna
             if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) &&
                 (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase)))
             {
-
                 if (width.HasValue && height.HasValue)
                 {
                     if ((width.Value <= 720) && (height.Value <= 576))
@@ -479,7 +477,9 @@ namespace MediaBrowser.Model.Dlna
         {
             if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) ||
                 string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase))
+            {
                 return ResolveImageJPGFormat(width, height);
+            }
 
             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 int MaxWidth { get; set; }
-
-        public int MaxBitrate { get; set; }
-
         public ResolutionConfiguration(int maxWidth, int maxBitrate)
         {
             MaxWidth = maxWidth;
             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 ResponseProfile()
+        {
+            Conditions = Array.Empty<ProfileCondition>();
+        }
+
         [XmlAttribute("container")]
         public string Container { get; set; }
 
@@ -28,11 +33,6 @@ namespace MediaBrowser.Model.Dlna
 
         public ProfileCondition[] Conditions { get; set; }
 
-        public ResponseProfile()
-        {
-            Conditions = Array.Empty<ProfileCondition>();
-        }
-
         public string[] GetContainers()
         {
             return ContainerProfile.SplitValue(Container);

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

@@ -7,31 +7,6 @@ namespace MediaBrowser.Model.Dlna
 {
     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)
         {
             if (search.Length == 0)
@@ -48,8 +23,8 @@ namespace MediaBrowser.Model.Dlna
 
                 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))
                         {
@@ -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 SortOrder SortOrder => SortOrder.Ascending;
-
         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))
             {
@@ -274,14 +274,14 @@ namespace MediaBrowser.Model.Dlna
             if (options.ForceDirectPlay)
             {
                 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;
             }
 
             if (options.ForceDirectStream)
             {
                 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;
             }
 
@@ -349,7 +349,7 @@ namespace MediaBrowser.Model.Dlna
                         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;
                 }
@@ -698,7 +698,7 @@ namespace MediaBrowser.Model.Dlna
                 if (directPlay != null)
                 {
                     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)
                     {
@@ -1404,7 +1404,9 @@ namespace MediaBrowser.Model.Dlna
             {
                 _logger.LogInformation(
                     "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
-                    playMethod, itemBitrate, requestedMaxBitrate);
+                    playMethod,
+                    itemBitrate,
+                    requestedMaxBitrate);
                 return false;
             }
 

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

@@ -27,45 +27,6 @@ namespace MediaBrowser.Model.Dlna
             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 PlayMethod PlayMethod { get; set; }
@@ -152,887 +113,906 @@ namespace MediaBrowser.Model.Dlna
             PlayMethod == PlayMethod.DirectStream ||
             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>
-        /// Returns the audio stream that will be used.
+        /// Gets the audio channels that will be in the output stream.
         /// </summary>
-        public MediaStream TargetAudioStream
+        public long? TargetSize
         {
             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;
             }
         }
 
-        /// <summary>
-        /// Returns the video stream that will be used.
-        /// </summary>
-        public MediaStream TargetVideoStream
+        public int? TargetVideoBitrate
         {
             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
             {
-                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
             {
                 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
             {
                 if (IsDirectStream)
                 {
-                    return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+                    return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
                 }
 
                 var targetVideoCodecs = TargetVideoCodec;
                 var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
                 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
             {
                 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
             {
-                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
             {
                 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))
             {
-                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;
         }
 
-        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)

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

@@ -294,13 +294,13 @@ namespace MediaBrowser.Model.Dto
         public NameGuidPair[] GenreItems { get; set; }
 
         /// <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>
         /// <value>The parent logo item id.</value>
         public string ParentLogoItemId { get; set; }
 
         /// <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>
         /// <value>The parent backdrop item id.</value>
         public string ParentBackdropItemId { get; set; }
@@ -318,7 +318,7 @@ namespace MediaBrowser.Model.Dto
         public int? LocalTrailerCount { get; set; }
 
         /// <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>
         /// <value>The user data.</value>
         public UserItemDataDto UserData { get; set; }
@@ -506,7 +506,7 @@ namespace MediaBrowser.Model.Dto
         public string ParentLogoImageTag { get; set; }
 
         /// <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>
         /// <value>The parent art item id.</value>
         public string ParentArtItemId { get; set; }
@@ -695,7 +695,7 @@ namespace MediaBrowser.Model.Dto
         public string ChannelPrimaryImageTag { get; set; }
 
         /// <summary>
-        /// The start date of the recording, in UTC.
+        /// Gets or sets the start date of the recording, in UTC.
         /// </summary>
         public DateTime? StartDate { get; set; }
 

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

@@ -12,6 +12,18 @@ namespace MediaBrowser.Model.Dto
 {
     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 string Id { get; set; }
@@ -31,6 +43,7 @@ namespace MediaBrowser.Model.Dto
         public string Name { get; set; }
 
         /// <summary>
+        /// Gets or sets a value indicating whether the media is remote.
         /// Differentiate internet url vs local network.
         /// </summary>
         public bool IsRemote { get; set; }
@@ -95,16 +108,28 @@ namespace MediaBrowser.Model.Dto
 
         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)
@@ -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)
         {
             if (defaultIndex.HasValue)
@@ -175,23 +193,6 @@ namespace MediaBrowser.Model.Dto
             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)
         {
             foreach (var i in MediaStreams)

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

@@ -10,6 +10,15 @@ namespace MediaBrowser.Model.Dto
 {
     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 CountryInfo[] Countries { get; set; }
@@ -21,14 +30,5 @@ namespace MediaBrowser.Model.Dto
         public string ContentType { 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>
         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>
     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>
         /// Gets or sets the name.
         /// </summary>
@@ -94,15 +103,6 @@ namespace MediaBrowser.Model.Dto
         /// <value>The primary image aspect ratio.</value>
         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 />
         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 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";
-    }
 }

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

@@ -84,7 +84,7 @@ namespace MediaBrowser.Model.Entities
         public string Title { get; set; }
 
         /// <summary>
-        /// Gets or sets the video range.
+        /// Gets the video range.
         /// </summary>
         /// <value>The video range.</value>
         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
         {
@@ -154,7 +154,7 @@ namespace MediaBrowser.Model.Entities
 
                         if (IsDefault)
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
                         }
 
                         if (!string.IsNullOrEmpty(Title))
@@ -229,17 +229,17 @@ namespace MediaBrowser.Model.Entities
                         }
                         else
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedUndefined) ? "Und" : LocalizedUndefined);
                         }
 
                         if (IsDefault)
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
                         }
 
                         if (IsForced)
                         {
-                            attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
+                            attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced);
                         }
 
                         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; }
 
         /// <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)
         {
             string codec = format ?? string.Empty;
@@ -533,35 +562,5 @@ namespace MediaBrowser.Model.Entities
 
             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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 
 namespace MediaBrowser.Model.Entities
 {
@@ -9,14 +10,26 @@ namespace MediaBrowser.Model.Entities
     public static class ProviderIdsExtensions
     {
         /// <summary>
-        /// Determines whether [has provider identifier] [the specified instance].
+        /// Gets a provider id.
         /// </summary>
         /// <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>
@@ -24,10 +37,11 @@ namespace MediaBrowser.Model.Entities
         /// </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)
+        /// <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>
@@ -38,18 +52,19 @@ namespace MediaBrowser.Model.Entities
         /// <returns>System.String.</returns>
         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>
@@ -68,13 +83,7 @@ namespace MediaBrowser.Model.Entities
             // If it's null remove the key from the dictionary
             if (string.IsNullOrEmpty(value))
             {
-                if (instance.ProviderIds != null)
-                {
-                    if (instance.ProviderIds.ContainsKey(name))
-                    {
-                        instance.ProviderIds.Remove(name);
-                    }
-                }
+                instance.ProviderIds?.Remove(name);
             }
             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";
+    }
+}

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

@@ -11,6 +11,14 @@ namespace MediaBrowser.Model.Entities
     /// </summary>
     public class VirtualFolderInfo
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
+        /// </summary>
+        public VirtualFolderInfo()
+        {
+            Locations = Array.Empty<string>();
+        }
+
         /// <summary>
         /// Gets or sets the name.
         /// </summary>
@@ -31,14 +39,6 @@ namespace MediaBrowser.Model.Entities
 
         public LibraryOptions LibraryOptions { get; set; }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
-        /// </summary>
-        public VirtualFolderInfo()
-        {
-            Locations = Array.Empty<string>();
-        }
-
         /// <summary>
         /// Gets or sets the item identifier.
         /// </summary>

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

@@ -10,6 +10,11 @@ namespace MediaBrowser.Model.Globalization
     /// </summary>
     public class CultureDto
     {
+        public CultureDto()
+        {
+            ThreeLetterISOLanguageNames = Array.Empty<string>();
+        }
+
         /// <summary>
         /// Gets or sets the name.
         /// </summary>
@@ -29,7 +34,7 @@ namespace MediaBrowser.Model.Globalization
         public string TwoLetterISOLanguageName { get; set; }
 
         /// <summary>
-        /// Gets or sets the name of the three letter ISO language.
+        /// Gets the name of the three letter ISO language.
         /// </summary>
         /// <value>The name of the three letter ISO language.</value>
         public string ThreeLetterISOLanguageName
@@ -47,10 +52,5 @@ namespace MediaBrowser.Model.Globalization
         }
 
         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.
         /// </summary>
         /// <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);
 
         /// <summary>
         /// Gets the files.
         /// </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, 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
     {
         /// <summary>
-        /// Id of the recording.
+        /// Gets or sets the Id of the recording.
         /// </summary>
         public string Id { get; set; }
 
@@ -28,7 +28,7 @@ namespace MediaBrowser.Model.LiveTv
         public string ExternalId { get; set; }
 
         /// <summary>
-        /// ChannelId of the recording.
+        /// Gets or sets the channel id of the recording.
         /// </summary>
         public Guid ChannelId { get; set; }
 
@@ -39,7 +39,7 @@ namespace MediaBrowser.Model.LiveTv
         public string ExternalChannelId { get; set; }
 
         /// <summary>
-        /// ChannelName of the recording.
+        /// Gets or sets the channel name of the recording.
         /// </summary>
         public string ChannelName { get; set; }
 
@@ -58,22 +58,22 @@ namespace MediaBrowser.Model.LiveTv
         public string ExternalProgramId { get; set; }
 
         /// <summary>
-        /// Name of the recording.
+        /// Gets or sets the name of the recording.
         /// </summary>
         public string Name { get; set; }
 
         /// <summary>
-        /// Description of the recording.
+        /// Gets or sets the description of the recording.
         /// </summary>
         public string Overview { get; set; }
 
         /// <summary>
-        /// The start date of the recording, in UTC.
+        /// Gets or sets the start date of the recording, in UTC.
         /// </summary>
         public DateTime StartDate { get; set; }
 
         /// <summary>
-        /// The end date of the recording, in UTC.
+        /// Gets or sets the end date of the recording, in UTC.
         /// </summary>
         public DateTime EndDate { get; set; }
 
@@ -108,7 +108,7 @@ namespace MediaBrowser.Model.LiveTv
         public bool IsPrePaddingRequired { get; set; }
 
         /// <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>
         /// <value>The parent backdrop item id.</value>
         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>
     public class LiveTvChannelQuery
     {
+        public LiveTvChannelQuery()
+        {
+            EnableUserData = true;
+            SortBy = Array.Empty<string>();
+        }
+
         /// <summary>
         /// Gets or sets the type of the channel.
         /// </summary>
@@ -48,13 +54,13 @@ namespace MediaBrowser.Model.LiveTv
         public Guid UserId { get; set; }
 
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Used for paging.
         /// </summary>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
 
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
@@ -68,15 +74,15 @@ namespace MediaBrowser.Model.LiveTv
         public bool EnableUserData { get; set; }
 
         /// <summary>
-        /// Used to specific whether to return news or not.
+        /// Gets or sets a value whether to return news or not.
         /// </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; }
 
         /// <summary>
-        /// Used to specific whether to return movies or not.
+        /// Gets or sets a value whether to return movies or not.
         /// </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; }
 
         /// <summary>
@@ -96,15 +102,9 @@ namespace MediaBrowser.Model.LiveTv
         public string[] SortBy { get; set; }
 
         /// <summary>
-        /// The sort order to return results with.
+        /// Gets or sets the sort order to return results with.
         /// </summary>
         /// <value>The sort order.</value>
         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
 
 using System;
-using MediaBrowser.Model.Dto;
 
 namespace MediaBrowser.Model.LiveTv
 {
     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 string RecordingPath { get; set; }
@@ -33,93 +40,5 @@ namespace MediaBrowser.Model.LiveTv
         public string RecordingPostProcessor { 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>
     public class LiveTvServiceInfo
     {
+        public LiveTvServiceInfo()
+        {
+            Tuners = Array.Empty<string>();
+        }
+
         /// <summary>
         /// Gets or sets the name.
         /// </summary>
@@ -53,10 +58,5 @@ namespace MediaBrowser.Model.LiveTv
         public bool IsVisible { 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>
     public class RecordingQuery
     {
+        public RecordingQuery()
+        {
+            EnableTotalRecordCount = true;
+        }
+
         /// <summary>
         /// Gets or sets the channel identifier.
         /// </summary>
@@ -31,13 +36,13 @@ namespace MediaBrowser.Model.LiveTv
         public string Id { get; set; }
 
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Use for paging.
         /// </summary>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
 
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
@@ -61,7 +66,7 @@ namespace MediaBrowser.Model.LiveTv
         public string SeriesTimerId { get; set; }
 
         /// <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>
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }
@@ -85,10 +90,5 @@ namespace MediaBrowser.Model.LiveTv
         public ImageType[] EnableImageTypes { 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
 {
+    public enum KeepUntil
+    {
+        UntilDeleted,
+        UntilSpaceNeeded,
+        UntilWatched,
+        UntilDate
+    }
+
     /// <summary>
     /// Class SeriesTimerInfoDto.
     /// </summary>
@@ -83,12 +91,4 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The parent primary image tag.</value>
         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>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <LangVersion>latest</LangVersion>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -44,7 +44,7 @@
 
   <!-- Code Analyzers-->
   <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="StyleCop.Analyzers" Version="1.1.118" 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 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; }
 
         /// <summary>
@@ -69,16 +80,5 @@ namespace MediaBrowser.Model.MediaInfo
         /// </summary>
         /// <value>The overview.</value>
         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 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 UserId { get; set; }
@@ -43,16 +54,5 @@ namespace MediaBrowser.Model.MediaInfo
         public bool AutoOpenLiveStream { 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 };
-        }
     }
 }

+ 8 - 8
MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs

@@ -10,6 +10,14 @@ namespace MediaBrowser.Model.MediaInfo
     /// </summary>
     public class PlaybackInfoResponse
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PlaybackInfoResponse" /> class.
+        /// </summary>
+        public PlaybackInfoResponse()
+        {
+            MediaSources = Array.Empty<MediaSourceInfo>();
+        }
+
         /// <summary>
         /// Gets or sets the media sources.
         /// </summary>
@@ -27,13 +35,5 @@ namespace MediaBrowser.Model.MediaInfo
         /// </summary>
         /// <value>The error code.</value>
         public PlaybackErrorCode? ErrorCode { get; set; }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PlaybackInfoResponse" /> class.
-        /// </summary>
-        public PlaybackInfoResponse()
-        {
-            MediaSources = Array.Empty<MediaSourceInfo>();
-        }
     }
 }

+ 6 - 1
MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs

@@ -1,10 +1,15 @@
-#nullable disable
 #pragma warning disable CS1591
 
 namespace MediaBrowser.Model.MediaInfo
 {
     public class SubtitleTrackEvent
     {
+        public SubtitleTrackEvent(string id, string text)
+        {
+            Id = id;
+            Text = text;
+        }
+
         public string Id { get; set; }
 
         public string Text { get; set; }

+ 3 - 2
MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -7,11 +8,11 @@ namespace MediaBrowser.Model.MediaInfo
 {
     public class SubtitleTrackInfo
     {
-        public IReadOnlyList<SubtitleTrackEvent> TrackEvents { get; set; }
-
         public SubtitleTrackInfo()
         {
             TrackEvents = Array.Empty<SubtitleTrackEvent>();
         }
+
+        public IReadOnlyList<SubtitleTrackEvent> TrackEvents { get; set; }
     }
 }

+ 6 - 0
MediaBrowser.Model/Net/ISocket.cs

@@ -23,6 +23,12 @@ namespace MediaBrowser.Model.Net
         /// <summary>
         /// Sends a UDP message to a particular end point (uni or multicast).
         /// </summary>
+        /// <param name="buffer">An array of type <see cref="byte" /> that contains the data to send.</param>
+        /// <param name="offset">The zero-based position in buffer at which to begin sending data.</param>
+        /// <param name="bytes">The number of bytes to send.</param>
+        /// <param name="endPoint">An <see cref="IPEndPoint" /> that represents the remote device.</param>
+        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+        /// <returns>The task object representing the asynchronous operation.</returns>
         Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken);
     }
 }

+ 3 - 0
MediaBrowser.Model/Net/ISocketFactory.cs

@@ -14,6 +14,9 @@ namespace MediaBrowser.Model.Net
         /// <summary>
         /// Creates a new unicast socket using the specified local port number.
         /// </summary>
+        /// <param name="localIp">The local IP address to bind to.</param>
+        /// <param name="localPort">The local port to bind to.</param>
+        /// <returns>A new unicast socket using the specified local port number.</returns>
         ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort);
 
         /// <summary>

+ 12 - 9
MediaBrowser.Model/Net/MimeTypes.cs

@@ -91,9 +91,9 @@ namespace MediaBrowser.Model.Net
             { ".webp", "image/webp" },
 
             // Type font
-            { ".ttf" , "font/ttf" },
-            { ".woff" , "font/woff" },
-            { ".woff2" , "font/woff2" },
+            { ".ttf", "font/ttf" },
+            { ".woff", "font/woff" },
+            { ".woff2", "font/woff2" },
 
             // Type text
             { ".ass", "text/x-ssa" },
@@ -168,14 +168,17 @@ namespace MediaBrowser.Model.Net
         /// <summary>
         /// Gets the type of the MIME.
         /// </summary>
-        public static string? GetMimeType(string path, bool enableStreamDefault)
+        /// <param name="filename">The filename to find the MIME type of.</param>
+        /// <param name="enableStreamDefault">Whether of not to return a default value if no fitting MIME type is found.</param>
+        /// <returns>The worrect MIME type for the given filename, or `null` if it wasn't found and <paramref name="enableStreamDefault"/> is false.</returns>
+        public static string? GetMimeType(string filename, bool enableStreamDefault)
         {
-            if (path.Length == 0)
+            if (filename.Length == 0)
             {
-                throw new ArgumentException("String can't be empty.", nameof(path));
+                throw new ArgumentException("String can't be empty.", nameof(filename));
             }
 
-            var ext = Path.GetExtension(path);
+            var ext = Path.GetExtension(filename);
 
             if (_mimeTypeLookup.TryGetValue(ext, out string? result))
             {
@@ -210,9 +213,9 @@ namespace MediaBrowser.Model.Net
             return enableStreamDefault ? "application/octet-stream" : null;
         }
 
-        public static string? ToExtension(string? mimeType)
+        public static string? ToExtension(string mimeType)
         {
-            if (string.IsNullOrEmpty(mimeType))
+            if (mimeType.Length == 0)
             {
                 throw new ArgumentException("String can't be empty.", nameof(mimeType));
             }

+ 0 - 33
MediaBrowser.Model/Net/NetworkShare.cs

@@ -1,33 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Net
-{
-    public class NetworkShare
-    {
-        /// <summary>
-        /// The name of the computer that this share belongs to.
-        /// </summary>
-        public string Server { get; set; }
-
-        /// <summary>
-        /// Share name.
-        /// </summary>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Local path.
-        /// </summary>
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Share type.
-        /// </summary>
-        public NetworkShareType ShareType { get; set; }
-
-        /// <summary>
-        /// Comment.
-        /// </summary>
-        public string Remark { get; set; }
-    }
-}

+ 2 - 2
MediaBrowser.Model/Net/SocketReceiveResult.cs

@@ -20,12 +20,12 @@ namespace MediaBrowser.Model.Net
         public int ReceivedBytes { get; set; }
 
         /// <summary>
-        /// The <see cref="IPEndPoint"/> the data was received from.
+        /// Gets or sets the <see cref="IPEndPoint"/> the data was received from.
         /// </summary>
         public IPEndPoint RemoteEndPoint { get; set; }
 
         /// <summary>
-        /// The local <see cref="IPAddress"/>.
+        /// Gets or sets the local <see cref="IPAddress"/>.
         /// </summary>
         public IPAddress LocalIPAddress { get; set; }
     }

+ 1 - 1
MediaBrowser.Model/Net/WebSocketMessage.cs

@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Net
     /// <summary>
     /// Class WebSocketMessage.
     /// </summary>
-    /// <typeparam name="T"></typeparam>
+    /// <typeparam name="T">The type of the data.</typeparam>
     public class WebSocketMessage<T>
     {
         /// <summary>

+ 5 - 5
MediaBrowser.Model/Notifications/NotificationOptions.cs

@@ -2,18 +2,16 @@
 #pragma warning disable CS1591
 
 using System;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Model.Extensions;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Model.Notifications
 {
     public class NotificationOptions
     {
-        public NotificationOption[] Options { get; set; }
-
         public NotificationOptions()
         {
             Options = new[]
@@ -71,6 +69,8 @@ namespace MediaBrowser.Model.Notifications
             };
         }
 
+        public NotificationOption[] Options { get; set; }
+
         public NotificationOption GetOptions(string type)
         {
             foreach (NotificationOption i in Options)
@@ -104,7 +104,7 @@ namespace MediaBrowser.Model.Notifications
             NotificationOption opt = GetOptions(type);
 
             return opt != null && opt.Enabled &&
-                   !opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase);
+                   !opt.DisabledMonitorUsers.Contains(userId.ToString(string.Empty), StringComparer.OrdinalIgnoreCase);
         }
 
         public bool IsEnabledToSendToUser(string type, string userId, User user)

+ 7 - 7
MediaBrowser.Model/Notifications/NotificationRequest.cs

@@ -7,6 +7,12 @@ namespace MediaBrowser.Model.Notifications
 {
     public class NotificationRequest
     {
+        public NotificationRequest()
+        {
+            UserIds = Array.Empty<Guid>();
+            Date = DateTime.UtcNow;
+        }
+
         public string Name { get; set; }
 
         public string Description { get; set; }
@@ -20,16 +26,10 @@ namespace MediaBrowser.Model.Notifications
         public DateTime Date { get; set; }
 
         /// <summary>
-        /// The corresponding type name used in configuration. Not for display.
+        /// Gets or sets the corresponding type name used in configuration. Not for display.
         /// </summary>
         public string NotificationType { get; set; }
 
         public SendToUserType? SendToUserMode { get; set; }
-
-        public NotificationRequest()
-        {
-            UserIds = Array.Empty<Guid>();
-            Date = DateTime.UtcNow;
-        }
     }
 }

+ 2 - 2
MediaBrowser.Model/Providers/ExternalIdInfo.cs

@@ -6,11 +6,11 @@ namespace MediaBrowser.Model.Providers
     public class ExternalIdInfo
     {
         /// <summary>
-        /// Represents the external id information for serialization to the client.
+        /// Initializes a new instance of the <see cref="ExternalIdInfo"/> class.
         /// </summary>
         /// <param name="name">Name of the external id provider (IE: IMDB, MusicBrainz, etc).</param>
         /// <param name="key">Key for this id. This key should be unique across all providers.</param>
-        /// <param name="type">Specific media type for this id</param>
+        /// <param name="type">Specific media type for this id.</param>
         /// <param name="urlFormatString">URL format string.</param>
         public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string urlFormatString)
         {

+ 1 - 1
MediaBrowser.Model/Providers/RemoteImageInfo.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Providers
         public string Url { get; set; }
 
         /// <summary>
-        /// Gets a url used for previewing a smaller version.
+        /// Gets or sets a url used for previewing a smaller version.
         /// </summary>
         public string ThumbnailUrl { get; set; }
 

+ 8 - 8
MediaBrowser.Model/Providers/SubtitleOptions.cs

@@ -7,6 +7,14 @@ namespace MediaBrowser.Model.Providers
 {
     public class SubtitleOptions
     {
+        public SubtitleOptions()
+        {
+            DownloadLanguages = Array.Empty<string>();
+
+            SkipIfAudioTrackMatches = true;
+            RequirePerfectMatch = true;
+        }
+
         public bool SkipIfEmbeddedSubtitlesPresent { get; set; }
 
         public bool SkipIfAudioTrackMatches { get; set; }
@@ -24,13 +32,5 @@ namespace MediaBrowser.Model.Providers
         public bool IsOpenSubtitleVipAccount { get; set; }
 
         public bool RequirePerfectMatch { get; set; }
-
-        public SubtitleOptions()
-        {
-            DownloadLanguages = Array.Empty<string>();
-
-            SkipIfAudioTrackMatches = true;
-            RequirePerfectMatch = true;
-        }
     }
 }

+ 5 - 5
MediaBrowser.Model/Querying/EpisodeQuery.cs

@@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Querying
 {
     public class EpisodeQuery
     {
+        public EpisodeQuery()
+        {
+            Fields = Array.Empty<ItemFields>();
+        }
+
         /// <summary>
         /// Gets or sets the user identifier.
         /// </summary>
@@ -66,10 +71,5 @@ namespace MediaBrowser.Model.Querying
         /// </summary>
         /// <value>The start item identifier.</value>
         public string StartItemId { get; set; }
-
-        public EpisodeQuery()
-        {
-            Fields = Array.Empty<ItemFields>();
-        }
     }
 }

+ 5 - 4
MediaBrowser.Model/Querying/LatestItemsQuery.cs

@@ -14,31 +14,32 @@ namespace MediaBrowser.Model.Querying
         }
 
         /// <summary>
-        /// The user to localize search results for.
+        /// Gets or sets the user to localize search results for.
         /// </summary>
         /// <value>The user id.</value>
         public Guid UserId { get; set; }
 
         /// <summary>
+        /// Gets or sets the parent id.
         /// Specify this to localize the search to a specific item or folder. Omit to use the root.
         /// </summary>
         /// <value>The parent id.</value>
         public Guid ParentId { get; set; }
 
         /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
+        /// Gets or sets the start index. Used for paging.
         /// </summary>
         /// <value>The start index.</value>
         public int? StartIndex { get; set; }
 
         /// <summary>
-        /// The maximum number of items to return.
+        /// Gets or sets the maximum number of items to return.
         /// </summary>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
 
         /// <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>
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }

+ 7 - 7
MediaBrowser.Model/Querying/MovieRecommendationQuery.cs

@@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Querying
 {
     public class MovieRecommendationQuery
     {
+        public MovieRecommendationQuery()
+        {
+            ItemLimit = 10;
+            CategoryLimit = 6;
+            Fields = Array.Empty<ItemFields>();
+        }
+
         /// <summary>
         /// Gets or sets the user identifier.
         /// </summary>
@@ -36,12 +43,5 @@ namespace MediaBrowser.Model.Querying
         /// </summary>
         /// <value>The fields.</value>
         public ItemFields[] Fields { get; set; }
-
-        public MovieRecommendationQuery()
-        {
-            ItemLimit = 10;
-            CategoryLimit = 6;
-            Fields = Array.Empty<ItemFields>();
-        }
     }
 }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff