Bläddra i källkod

merge branch 'master' into auto-manifest

dkanada 4 år sedan
förälder
incheckning
bc746b4d05
100 ändrade filer med 1829 tillägg och 2378 borttagningar
  1. 0 1
      Emby.Dlna/ContentDirectory/StubType.cs
  2. 1 1
      Emby.Dlna/DlnaManager.cs
  3. 4 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  4. 46 9
      Emby.Dlna/PlayTo/Device.cs
  5. 1 1
      Emby.Dlna/PlayTo/PlayToController.cs
  6. 0 1
      Emby.Dlna/PlayTo/TransportState.cs
  7. 1 1
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  8. 1 1
      Emby.Naming/TV/SeasonPathParser.cs
  9. 1 1
      Emby.Server.Implementations/ApplicationHost.cs
  10. 3 3
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  11. 45 45
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  12. 1 1
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  13. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  14. 4 3
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  15. 0 5
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  16. 9 10
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  17. 0 5
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  18. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  19. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  20. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  21. 11 11
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  22. 6 6
      Emby.Server.Implementations/Localization/Core/sr.json
  23. 2 2
      Emby.Server.Implementations/Localization/Core/vi.json
  24. 1 1
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  25. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  26. 0 1
      Emby.Server.Implementations/Session/WebSocketController.cs
  27. 11 32
      Jellyfin.Api/Controllers/DashboardController.cs
  28. 2 2
      Jellyfin.Api/Controllers/ImageController.cs
  29. 1 3
      Jellyfin.Api/Controllers/PluginsController.cs
  30. 2 2
      Jellyfin.Api/Controllers/UniversalAudioController.cs
  31. 0 9
      Jellyfin.Api/Extensions/DtoExtensions.cs
  32. 8 5
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  33. 1 1
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  34. 1 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  35. 2 2
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  36. 8 2
      Jellyfin.Api/Jellyfin.Api.csproj
  37. 8 1
      Jellyfin.Api/Models/ConfigurationPageInfo.cs
  38. 1 1
      Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
  39. 23 0
      Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs
  40. 20 8
      Jellyfin.Server.Implementations/Users/UserManager.cs
  41. 0 1
      Jellyfin.Server/CoreAppHost.cs
  42. 1 0
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  43. 1 1
      Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
  44. 7 0
      Jellyfin.sln
  45. 51 0
      MediaBrowser.Common/Extensions/StreamExtensions.cs
  46. 0 37
      MediaBrowser.Common/Extensions/StringExtensions.cs
  47. 0 1
      MediaBrowser.Common/Json/JsonDefaults.cs
  48. 0 5
      MediaBrowser.Common/Net/NetworkExtensions.cs
  49. 0 3
      MediaBrowser.Common/Plugins/BasePlugin.cs
  50. 2 2
      MediaBrowser.Controller/Entities/BaseItem.cs
  51. 1 1
      MediaBrowser.Controller/Entities/TV/Series.cs
  52. 15 8
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  53. 1 1
      MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
  54. 3 3
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  55. 1 0
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  56. 5 5
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  57. 12 121
      MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
  58. 11 92
      MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
  59. 11 467
      MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
  60. 63 0
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
  61. 2 5
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  62. 9 9
      MediaBrowser.Model/Channels/ChannelFeatures.cs
  63. 3 3
      MediaBrowser.Model/Channels/ChannelQuery.cs
  64. 37 37
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  65. 5 5
      MediaBrowser.Model/Configuration/ImageOption.cs
  66. 19 384
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  67. 12 0
      MediaBrowser.Model/Configuration/MediaPathInfo.cs
  68. 2 2
      MediaBrowser.Model/Configuration/MetadataConfiguration.cs
  69. 10 10
      MediaBrowser.Model/Configuration/MetadataOptions.cs
  70. 6 6
      MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
  71. 1 8
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  72. 365 0
      MediaBrowser.Model/Configuration/TypeOptions.cs
  73. 18 18
      MediaBrowser.Model/Configuration/UserConfiguration.cs
  74. 8 8
      MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
  75. 5 4
      MediaBrowser.Model/Dlna/AudioOptions.cs
  76. 6 6
      MediaBrowser.Model/Dlna/CodecProfile.cs
  77. 1 1
      MediaBrowser.Model/Dlna/ConditionProcessor.cs
  78. 5 5
      MediaBrowser.Model/Dlna/ContainerProfile.cs
  79. 19 17
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  80. 1 0
      MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
  81. 2 2
      MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
  82. 4 4
      MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
  83. 5 5
      MediaBrowser.Model/Dlna/ResponseProfile.cs
  84. 27 27
      MediaBrowser.Model/Dlna/SearchCriteria.cs
  85. 2 2
      MediaBrowser.Model/Dlna/SortCriteria.cs
  86. 9 7
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  87. 576 596
      MediaBrowser.Model/Dlna/StreamInfo.cs
  88. 5 5
      MediaBrowser.Model/Dto/BaseItemDto.cs
  89. 34 33
      MediaBrowser.Model/Dto/MediaSourceInfo.cs
  90. 9 9
      MediaBrowser.Model/Dto/MetadataEditorInfo.cs
  91. 14 0
      MediaBrowser.Model/Dto/NameGuidPair.cs
  92. 0 7
      MediaBrowser.Model/Dto/NameIdPair.cs
  93. 9 9
      MediaBrowser.Model/Dto/UserDto.cs
  94. 0 32
      MediaBrowser.Model/Entities/CollectionType.cs
  95. 99 100
      MediaBrowser.Model/Entities/MediaStream.cs
  96. 0 40
      MediaBrowser.Model/Entities/PackageReviewInfo.cs
  97. 35 26
      MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
  98. 36 0
      MediaBrowser.Model/Entities/SpecialFolder.cs
  99. 8 8
      MediaBrowser.Model/Entities/VirtualFolderInfo.cs
  100. 6 6
      MediaBrowser.Model/Globalization/CultureDto.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(),

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

@@ -228,7 +228,10 @@ namespace Emby.Dlna.Main
         {
             try
             {
-                ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+                if (communicationsServer != null)
+                {
+                    ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+                }
             }
             catch (Exception ex)
             {

+ 46 - 9
Emby.Dlna/PlayTo/Device.cs

@@ -235,7 +235,13 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    rendererCommands.BuildPost(command, service.ServiceType, value),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
             IsMuted = mute;
@@ -270,7 +276,13 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             Volume = value;
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    rendererCommands.BuildPost(command, service.ServiceType, value),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
         }
 
@@ -291,7 +303,13 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -325,14 +343,21 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    post,
+                    header: header,
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
-            await Task.Delay(50).ConfigureAwait(false);
+            await Task.Delay(50, cancellationToken).ConfigureAwait(false);
 
             try
             {
-                await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
+                await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
             }
             catch
             {
@@ -396,7 +421,13 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    avCommands.BuildPost(command, service.ServiceType, 1),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
             RestartTimer(true);
@@ -414,7 +445,13 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    avCommands.BuildPost(command, service.ServiceType, 1),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
             TransportState = TransportState.Paused;
@@ -990,7 +1027,7 @@ namespace Emby.Dlna.PlayTo
 
             var deviceProperties = new DeviceInfo()
             {
-                Name = string.Join(" ", friendlyNames),
+                Name = string.Join(' ', friendlyNames),
                 BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
             };
 

+ 1 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -777,7 +777,7 @@ namespace Emby.Dlna.PlayTo
             var currentWait = 0;
             while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
             {
-                await Task.Delay(Interval).ConfigureAwait(false);
+                await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
                 currentWait += Interval;
             }
 

+ 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.Naming/AudioBook/AudioBookListResolver.cs

@@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
 
             var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
             var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
-            var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
+            var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
 
             foreach (var group in groupedBy)
             {

+ 1 - 1
Emby.Naming/TV/SeasonPathParser.cs

@@ -60,7 +60,7 @@ namespace Emby.Naming.TV
             bool supportSpecialAliases,
             bool supportNumericSeasonFolders)
         {
-            var filename = Path.GetFileName(path) ?? string.Empty;
+            string filename = Path.GetFileName(path);
 
             if (supportSpecialAliases)
             {

+ 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/SqliteExtensions.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using SQLitePCL.pretty;
 
@@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Data
 
             connection.RunInTransaction(conn =>
             {
-                conn.ExecuteAll(string.Join(";", queries));
+                conn.ExecuteAll(string.Join(';', queries));
             });
         }
 
@@ -142,11 +143,10 @@ namespace Emby.Server.Implementations.Data
             return result[index].ReadGuidFromBlob();
         }
 
+        [Conditional("DEBUG")]
         private static void CheckName(string name)
         {
-#if DEBUG
             throw new ArgumentException("Invalid param name: " + name, nameof(name));
-#endif
         }
 
         public static void TryBind(this IStatement statement, string name, double value)

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

@@ -687,7 +687,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.Genres.Length > 0)
             {
-                saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres));
+                saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
             }
             else
             {
@@ -749,7 +749,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.LockedFields.Length > 0)
             {
-                saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields));
+                saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
             }
             else
             {
@@ -758,7 +758,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.Studios.Length > 0)
             {
-                saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios));
+                saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
             }
             else
             {
@@ -785,7 +785,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.Tags.Length > 0)
             {
-                saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags));
+                saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
             }
             else
             {
@@ -807,7 +807,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
             {
-                saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes));
+                saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
             }
             else
             {
@@ -902,7 +902,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.ProductionLocations.Length > 0)
             {
-                saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations));
+                saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
             }
             else
             {
@@ -911,7 +911,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.ExtraIds.Length > 0)
             {
-                saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds));
+                saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
             }
             else
             {
@@ -931,7 +931,7 @@ namespace Emby.Server.Implementations.Data
             string artists = null;
             if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
             {
-                artists = string.Join("|", hasArtists.Artists);
+                artists = string.Join('|', hasArtists.Artists);
             }
 
             saveItemStatement.TryBind("@Artists", artists);
@@ -940,7 +940,7 @@ namespace Emby.Server.Implementations.Data
             if (item is IHasAlbumArtist hasAlbumArtists
                 && hasAlbumArtists.AlbumArtists.Count > 0)
             {
-                albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists);
+                albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
             }
 
             saveItemStatement.TryBind("@AlbumArtists", albumArtists);
@@ -2549,7 +2549,7 @@ namespace Emby.Server.Implementations.Data
 
             if (groups.Count > 0)
             {
-                return " Group by " + string.Join(",", groups);
+                return " Group by " + string.Join(',', groups);
             }
 
             return string.Empty;
@@ -2578,7 +2578,7 @@ namespace Emby.Server.Implementations.Data
             }
 
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
                             + GetFromText()
                             + GetJoinUserDataText(query);
 
@@ -2630,7 +2630,7 @@ namespace Emby.Server.Implementations.Data
             }
 
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
+                            + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
                             + GetFromText()
                             + GetJoinUserDataText(query);
 
@@ -2880,7 +2880,7 @@ namespace Emby.Server.Implementations.Data
             }
 
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
+                            + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
                             + GetFromText()
                             + GetJoinUserDataText(query);
 
@@ -2923,15 +2923,15 @@ namespace Emby.Server.Implementations.Data
 
                 if (EnableGroupByPresentationUniqueKey(query))
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
                 }
                 else if (query.GroupBySeriesPresentationUniqueKey)
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
                 }
                 else
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
                 }
 
                 commandText += GetJoinUserDataText(query)
@@ -3039,7 +3039,7 @@ namespace Emby.Server.Implementations.Data
                 return string.Empty;
             }
 
-            return " ORDER BY " + string.Join(",", orderBy.Select(i =>
+            return " ORDER BY " + string.Join(',', orderBy.Select(i =>
             {
                 var columnMap = MapOrderByField(i.Item1, query);
 
@@ -3137,7 +3137,7 @@ namespace Emby.Server.Implementations.Data
             var now = DateTime.UtcNow;
 
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
                             + GetFromText()
                             + GetJoinUserDataText(query);
 
@@ -3203,7 +3203,7 @@ namespace Emby.Server.Implementations.Data
 
             var now = DateTime.UtcNow;
 
-            var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+            var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
 
             var whereClauses = GetWhereClauses(query, null);
             if (whereClauses.Count != 0)
@@ -3284,7 +3284,7 @@ namespace Emby.Server.Implementations.Data
             var now = DateTime.UtcNow;
 
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
                             + GetFromText()
                             + GetJoinUserDataText(query);
 
@@ -3327,15 +3327,15 @@ namespace Emby.Server.Implementations.Data
 
                 if (EnableGroupByPresentationUniqueKey(query))
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
                 }
                 else if (query.GroupBySeriesPresentationUniqueKey)
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
                 }
                 else
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
                 }
 
                 commandText += GetJoinUserDataText(query)
@@ -3596,7 +3596,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 else if (excludeTypes.Length > 1)
                 {
-                    var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'"));
+                    var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'"));
                     whereClauses.Add($"type not in ({inClause})");
                 }
             }
@@ -3607,7 +3607,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (includeTypes.Length > 1)
             {
-                var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
+                var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'"));
                 whereClauses.Add($"type in ({inClause})");
             }
 
@@ -3618,7 +3618,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (query.ChannelIds.Count > 1)
             {
-                var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+                var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
                 whereClauses.Add($"ChannelId in ({inClause})");
             }
 
@@ -4351,7 +4351,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (query.Years.Length > 1)
             {
-                var val = string.Join(",", query.Years);
+                var val = string.Join(',', query.Years);
 
                 whereClauses.Add("ProductionYear in (" + val + ")");
             }
@@ -4401,7 +4401,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (queryMediaTypes.Length > 1)
             {
-                var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'"));
+                var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
 
                 whereClauses.Add("MediaType in (" + val + ")");
             }
@@ -4498,7 +4498,7 @@ namespace Emby.Server.Implementations.Data
                     var paramName = "@HasAnyProviderId" + index;
 
                     // this is a search for the placeholder
-                    hasProviderIds.Add("ProviderIds like " + paramName + "");
+                    hasProviderIds.Add("ProviderIds like " + paramName);
 
                     // this replaces the placeholder with a value, here: %key=val%
                     if (statement != null)
@@ -4549,7 +4549,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 else if (enableItemsByName && includedItemByNameTypes.Count > 1)
                 {
-                    var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
+                    var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
                     whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
                 }
                 else
@@ -4564,7 +4564,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (queryTopParentIds.Length > 1)
             {
-                var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+                var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
 
                 if (enableItemsByName && includedItemByNameTypes.Count == 1)
                 {
@@ -4576,7 +4576,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 else if (enableItemsByName && includedItemByNameTypes.Count > 1)
                 {
-                    var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
+                    var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
                     whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
                 }
                 else
@@ -4597,7 +4597,7 @@ namespace Emby.Server.Implementations.Data
 
             if (query.AncestorIds.Length > 1)
             {
-                var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+                var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
                 whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
             }
 
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
             }
             else if (queryPersonTypes.Count > 1)
             {
-                var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'"));
+                var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
 
                 whereClauses.Add("PersonType in (" + val + ")");
             }
@@ -5162,7 +5162,7 @@ AND Type = @InternalPersonType)");
             }
             else if (queryExcludePersonTypes.Count > 1)
             {
-                var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'"));
+                var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
 
                 whereClauses.Add("PersonType not in (" + val + ")");
             }
@@ -5308,19 +5308,19 @@ AND Type = @InternalPersonType)");
 
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
-                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+                ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
 
             var commandText = "Select Value From ItemValues where " + typeClause;
 
             if (withItemTypes.Count > 0)
             {
-                var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'"));
+                var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'"));
                 commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
             }
 
             if (excludeItemTypes.Count > 0)
             {
-                var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'"));
+                var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'"));
                 commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
             }
 
@@ -5363,7 +5363,7 @@ AND Type = @InternalPersonType)");
 
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
-                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+                ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
 
             InternalItemsQuery typeSubQuery = null;
 
@@ -5427,7 +5427,7 @@ AND Type = @InternalPersonType)");
             columns = GetFinalColumnsToSelect(query, columns);
 
             var commandText = "select "
-                            + string.Join(",", columns)
+                            + string.Join(',', columns)
                             + GetFromText()
                             + GetJoinUserDataText(query);
 
@@ -5504,7 +5504,7 @@ AND Type = @InternalPersonType)");
             if (query.EnableTotalRecordCount)
             {
                 var countText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
                             + GetFromText()
                             + GetJoinUserDataText(query)
                             + whereText;
@@ -5565,7 +5565,7 @@ AND Type = @InternalPersonType)");
                         if (query.EnableTotalRecordCount)
                         {
                             commandText = "select "
-                                        + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+                                        + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
                                         + GetFromText()
                                         + GetJoinUserDataText(query)
                                         + whereText;
@@ -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/Data/SqliteUserDataRepository.cs

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Data
                 connection.RunInTransaction(
                 db =>
                 {
-                    db.ExecuteAll(string.Join(";", new[] {
+                    db.ExecuteAll(string.Join(';', new[] {
 
                         "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
 

+ 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.27.1" />
+    <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 - 5
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return new MusicArtist();
             }
 
-            if (_config.Configuration.EnableSimpleArtistDetection)
-            {
-                return null;
-            }
-
             // Avoid mis-identifying top folders
             if (args.Parent.IsRoot)
             {

+ 9 - 10
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -35,8 +35,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly ICryptoProvider _cryptoProvider;
 
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
-        private DateTime _lastErrorResponse;
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private DateTime _lastErrorResponse;
 
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
@@ -111,7 +111,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             options.Headers.TryAddWithoutValidation("token", token);
             using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions).ConfigureAwait(false);
+            var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
 
             using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -122,12 +122,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
+            var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             var programDict = programDetails.ToDictionary(p => p.programID, y => y);
 
-            var programIdsWithImages =
-                programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
-                    .ToList();
+            var programIdsWithImages = programDetails
+                .Where(p => p.hasImageArtwork).Select(p => p.programID)
+                .ToList();
 
             var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
 
@@ -182,8 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
         private static int GetSizeOrder(ScheduleDirect.ImageData image)
         {
-            if (!string.IsNullOrWhiteSpace(image.height)
-                && int.TryParse(image.height, out int value))
+            if (int.TryParse(image.height, out int value))
             {
                 return value;
             }
@@ -704,7 +703,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 httpResponse.EnsureSuccessStatusCode();
                 await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
                 using var response = httpResponse.Content;
-                var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions).ConfigureAwait(false);
+                var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
             }
@@ -776,7 +775,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions).ConfigureAwait(false);
+            var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
             _logger.LogInformation("Mapping Stations to Channel");
 

+ 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];

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             {
                 try
                 {
-                    await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
+                    await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false);
                     localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
                     tcpClient.Close();
                 }

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -155,7 +155,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             if (channelIdValues.Count > 0)
             {
-                channel.Id = string.Join("_", channelIdValues);
+                channel.Id = string.Join('_', channelIdValues);
             }
 
             return channel;

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
                 EnableStreamSharing = false;
                 await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
-            });
+            }, CancellationToken.None);
         }
 
         private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)

+ 11 - 11
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -55,26 +55,26 @@
     "NotificationOptionPluginInstalled": "Приставката е инсталирана",
     "NotificationOptionPluginUninstalled": "Приставката е деинсталирана",
     "NotificationOptionPluginUpdateInstalled": "Обновлението на приставката е инсталирано",
-    "NotificationOptionServerRestartRequired": "Нужно е повторно пускане на сървъра",
+    "NotificationOptionServerRestartRequired": "Сървърът трябва да се рестартира",
     "NotificationOptionTaskFailed": "Грешка в планирана задача",
-    "NotificationOptionUserLockedOut": "Потребителя е заключен",
+    "NotificationOptionUserLockedOut": "Потребителят е заключен",
     "NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна",
     "NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
     "Photos": "Снимки",
     "Playlists": "Списъци",
     "Plugin": "Приставка",
-    "PluginInstalledWithName": "{0} е инсталирано",
-    "PluginUninstalledWithName": "{0} е деинсталирано",
-    "PluginUpdatedWithName": "{0} е обновено",
+    "PluginInstalledWithName": "{0} е инсталиранa",
+    "PluginUninstalledWithName": "{0} е деинсталиранa",
+    "PluginUpdatedWithName": "{0} е обновенa",
     "ProviderValue": "Доставчик: {0}",
     "ScheduledTaskFailedWithName": "{0} се провали",
     "ScheduledTaskStartedWithName": "{0} започна",
-    "ServerNameNeedsToBeRestarted": "{0} е нужно да се рестартира",
+    "ServerNameNeedsToBeRestarted": "{0} трябва да се рестартира",
     "Shows": "Сериали",
     "Songs": "Песни",
     "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
     "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
-    "SubtitleDownloadFailureFromForItem": "Поднадписите за {1} от {0} не можаха да се изтеглят",
+    "SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
     "Sync": "Синхронизиране",
     "System": "Система",
     "TvShows": "Телевизионни сериали",
@@ -92,12 +92,12 @@
     "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
     "ValueSpecialEpisodeName": "Специални - {0}",
     "VersionNumber": "Версия {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи поднадписи, на база конфигурацията за мета-данни.",
-    "TaskDownloadMissingSubtitles": "Изтегляне на липсващи поднадписи",
+    "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи субтитри, на база конфигурацията за мета-данни.",
+    "TaskDownloadMissingSubtitles": "Изтегляне на липсващи субтитри",
     "TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.",
     "TaskRefreshChannels": "Обновяване на Канали",
-    "TaskCleanTranscodeDescription": "Изтрива прекодирани файлове по-стари от един ден.",
-    "TaskCleanTranscode": "Изчиства директорията за прекодиране",
+    "TaskCleanTranscodeDescription": "Изтрива транскодирани файлове по-стари от един ден.",
+    "TaskCleanTranscode": "Изчиства директорията за транскодиране",
     "TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.",
     "TaskUpdatePlugins": "Актуализира добавките",
     "TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.",

+ 6 - 6
Emby.Server.Implementations/Localization/Core/sr.json

@@ -4,11 +4,11 @@
     "VersionNumber": "Верзија {0}",
     "ValueSpecialEpisodeName": "Специјал - {0}",
     "ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку",
-    "UserStoppedPlayingItemWithValues": "{0} заврши пуштање {1} на {2}",
+    "UserStoppedPlayingItemWithValues": "{0} завршио пуштање {1} на {2}",
     "UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}",
     "UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}",
     "UserOnlineFromDevice": "{0} је на вези од {1}",
-    "UserOfflineFromDevice": "{0} се одвезао са {1}",
+    "UserOfflineFromDevice": "{0} је прекинуо/а везу са {1}",
     "UserLockedOutWithName": "Корисник {0} је закључан",
     "UserDownloadingItemWithValues": "{0} преузима {1}",
     "UserDeletedWithName": "Корисник {0} је обрисан",
@@ -41,7 +41,7 @@
     "NotificationOptionPluginError": "Грешка прикључка",
     "NotificationOptionNewLibraryContent": "Додат нови садржај",
     "NotificationOptionInstallationFailed": "Неуспела инсталација",
-    "NotificationOptionCameraImageUploaded": "Слика са камере послата",
+    "NotificationOptionCameraImageUploaded": "Слика са камере отпремљена",
     "NotificationOptionAudioPlaybackStopped": "Заустављено пуштање звука",
     "NotificationOptionAudioPlayback": "Покренуто пуштање звука",
     "NotificationOptionApplicationUpdateInstalled": "Ажурирање инсталирано",
@@ -86,7 +86,7 @@
     "Channels": "Канали",
     "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
     "Books": "Књиге",
-    "AuthenticationSucceededWithUserName": "{0} успешно проверено",
+    "AuthenticationSucceededWithUserName": "{0} Успешна аутентикација",
     "Artists": "Извођачи",
     "Application": "Апликација",
     "AppDeviceValues": "Апликација: {0}, Уређај: {1}",
@@ -100,7 +100,7 @@
     "TaskUpdatePluginsDescription": "Преузима и инсталира исправке за додатке који су конфигурисани за аутоматско ажурирање.",
     "TaskUpdatePlugins": "Ажурирајте додатке",
     "TaskRefreshPeopleDescription": "Ажурира метаподатке за глумце и редитеље у вашој медијској библиотеци.",
-    "TaskRefreshPeople": "Освежите људе",
+    "TaskRefreshPeople": "Освежите кориснике",
     "TaskCleanLogsDescription": "Брише логове старије од {0} дана.",
     "TaskCleanLogs": "Очистите директоријум логова",
     "TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.",
@@ -116,6 +116,6 @@
     "TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
     "TaskCleanActivityLog": "Очисти историју активности",
     "Undefined": "Недефинисано",
-    "Forced": "Форсирано",
+    "Forced": "Принудно",
     "Default": "Подразумевано"
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/vi.json

@@ -3,7 +3,7 @@
     "Favorites": "Yêu Thích",
     "Folders": "Thư Mục",
     "Genres": "Thể Loại",
-    "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+    "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
     "HeaderContinueWatching": "Xem Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "Movies": "Phim",
@@ -13,7 +13,7 @@
     "Songs": "Các Bài Hát",
     "Sync": "Đồng Bộ",
     "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
-    "Albums": "Albums",
+    "Albums": "Tuyển Tập",
     "Artists": "Các Nghệ Sĩ",
     "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
     "TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",

+ 1 - 1
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -166,7 +166,7 @@ namespace Emby.Server.Implementations.MediaEncoder
                         }
                         catch (Exception ex)
                         {
-                            _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(",", video.Path));
+                            _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
                             success = false;
                             break;
                         }

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
                         Directory.CreateDirectory(parentPath);
 
-                        string text = string.Join("|", previouslyFailedImages);
+                        string text = string.Join('|', previouslyFailedImages);
                         File.WriteAllText(failHistoryPath, text);
                     }
 

+ 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)

+ 1 - 3
Jellyfin.Api/Controllers/PluginsController.cs

@@ -298,9 +298,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
-            if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
-                || plugin.Manifest.ImagePath == null
-                || !System.IO.File.Exists(imagePath))
+            if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath))
             {
                 return NotFound();
             }

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

@@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers
                     DeInterlace = true,
                     RequireNonAnamorphic = true,
                     EnableMpegtsM2TsMode = true,
-                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
                     Context = EncodingContext.Static,
                     StreamOptions = new Dictionary<string, string>(),
                     EnableAdaptiveBitrateStreaming = true
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
                 CopyTimestamps = true,
                 StartTimeTicks = startTimeTicks,
                 SubtitleMethod = SubtitleDeliveryMethod.Embed,
-                TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
                 Context = EncodingContext.Static
             };
 

+ 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);
     }
 }

+ 8 - 5
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;
             }
@@ -222,7 +225,7 @@ namespace Jellyfin.Api.Helpers
                     {
                         // Force HEVC Main Profile and disable video stream copy.
                         state.OutputVideoCodec = "hevc";
-                        var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main");
+                        var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
                         sdrVideoUrl += "&AllowVideoStreamCopy=false";
 
                         EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
@@ -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/FileStreamResponseHelpers.cs

@@ -39,7 +39,7 @@ namespace Jellyfin.Api.Helpers
             }
 
             // Can't dispose the response as it's required up the call chain.
-            var response = await httpClient.GetAsync(new Uri(state.MediaPath)).ConfigureAwait(false);
+            var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
             var contentType = response.Content.Headers.ContentType?.ToString();
 
             httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";

+ 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);
 

+ 8 - 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="5.6.3" />
-    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.2" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.7" />
+    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.7" />
   </ItemGroup>
 
   <ItemGroup>
@@ -38,4 +38,10 @@
     <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
+  <ItemGroup>
+    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
+      <_Parameter1>Jellyfin.Api.Tests</_Parameter1>
+    </AssemblyAttribute>
+  </ItemGroup>
+
 </Project>

+ 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)

+ 23 - 0
Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs

@@ -0,0 +1,23 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Jellyfin.Server.Implementations")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Jellyfin Project")]
+[assembly: AssemblyProduct("Jellyfin Server")]
+[assembly: AssemblyCopyright("Copyright ©  2019 Jellyfin Contributors. Code released under the GNU General Public License")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]

+ 20 - 8
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -137,17 +137,14 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentNullException(nameof(user));
             }
 
-            if (string.IsNullOrWhiteSpace(newName))
-            {
-                throw new ArgumentException("Invalid username", nameof(newName));
-            }
+            ThrowIfInvalidUsername(newName);
 
             if (user.Username.Equals(newName, StringComparison.Ordinal))
             {
                 throw new ArgumentException("The new and old names must be different.");
             }
 
-            if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
+            if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
             {
                 throw new ArgumentException(string.Format(
                     CultureInfo.InvariantCulture,
@@ -201,9 +198,14 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public async Task<User> CreateUserAsync(string name)
         {
-            if (!IsValidUsername(name))
+            ThrowIfInvalidUsername(name);
+
+            if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
             {
-                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+                throw new ArgumentException(string.Format(
+                    CultureInfo.InvariantCulture,
+                    "A user with the name '{0}' already exists.",
+                    name));
             }
 
             await using var dbContext = _dbProvider.CreateContext();
@@ -725,12 +727,22 @@ namespace Jellyfin.Server.Implementations.Users
             _users[user.Id] = user;
         }
 
+        internal static void ThrowIfInvalidUsername(string name)
+        {
+            if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name))
+            {
+                return;
+            }
+
+            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
+        }
+
         private static bool IsValidUsername(string name)
         {
             // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
             // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
             // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
-            return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
+            return Regex.IsMatch(name, @"^[\w\ \-'._@]+$");
         }
 
         private IAuthenticationProvider GetAuthenticationProvider(User user)

+ 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 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -310,6 +310,7 @@ namespace Jellyfin.Server.Extensions
 
                 // Allow parameters to properly be nullable.
                 c.UseAllOfToExtendReferenceSchemas();
+                c.SupportNonNullableReferenceTypes();
 
                 // TODO - remove when all types are supported in System.Text.Json
                 c.AddSwaggerTypeMappings();

+ 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");

+ 7 - 0
MediaBrowser.sln → Jellyfin.sln

@@ -74,6 +74,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "test
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -200,6 +202,10 @@ Global
 		{30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -214,6 +220,7 @@ Global
 		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

+ 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 - 1
MediaBrowser.Common/Json/JsonDefaults.cs

@@ -31,7 +31,6 @@ namespace MediaBrowser.Common.Json
             WriteIndented = false,
             DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
             NumberHandling = JsonNumberHandling.AllowReadingFromString,
-            PropertyNameCaseInsensitive = true,
             Converters =
             {
                 new JsonGuidConverter(),

+ 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
 {

+ 2 - 2
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1243,7 +1243,7 @@ namespace MediaBrowser.Controller.Entities
                 }
             }
 
-            return string.Join("/", terms.ToArray());
+            return string.Join('/', terms.ToArray());
         }
 
         /// <summary>
@@ -2795,7 +2795,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var list = GetEtagValues(user);
 
-            return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
         }
 
         protected virtual List<string> GetEtagValues(User user)

+ 1 - 1
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -107,7 +107,7 @@ namespace MediaBrowser.Controller.Entities.TV
                 return key;
             }
 
-            return key + "-" + string.Join("-", folders);
+            return key + "-" + string.Join('-', folders);
         }
 
         private static string GetUniqueSeriesKey(BaseItem series)

+ 15 - 8
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -131,6 +131,12 @@ namespace MediaBrowser.Controller.MediaEncoding
         private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
         {
             var videoStream = state.VideoStream;
+            if (videoStream == null)
+            {
+                // Remote stream doesn't have media info, disable vpp tonemapping.
+                return false;
+            }
+
             var codec = videoStream.Codec;
             if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
             {
@@ -592,7 +598,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 if (state.IsVideoRequest
                     && ((string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
                          && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder))
-                        || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) 
+                        || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
                             && (isD3d11vaDecoder || isSwDecoder))))
                 {
                     if (isTonemappingSupported)
@@ -1381,7 +1387,8 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 var requestedProfile = requestedProfiles[0];
                 // strip spaces because they may be stripped out on the query string as well
-                if (!string.IsNullOrEmpty(videoStream.Profile) && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", ""), StringComparer.OrdinalIgnoreCase))
+                if (!string.IsNullOrEmpty(videoStream.Profile)
+                    && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", "", StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
                 {
                     var currentScore = GetVideoProfileScore(videoStream.Profile);
                     var requestedScore = GetVideoProfileScore(requestedProfile);
@@ -1710,7 +1717,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (filters.Count > 0)
             {
-                return " -af \"" + string.Join(",", filters) + "\"";
+                return " -af \"" + string.Join(',', filters) + "\"";
             }
 
             return string.Empty;
@@ -2530,7 +2537,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
             // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
-            var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
+            var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.AverageFrameRate ?? 60) <= 30;
 
             var isScalingInAdvance = false;
             var isCudaDeintInAdvance = false;
@@ -2888,7 +2895,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 output += string.Format(
                     CultureInfo.InvariantCulture,
                     "{0}",
-                    string.Join(",", filters));
+                    string.Join(',', filters));
             }
 
             return output;
@@ -2914,7 +2921,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (threads <= 0)
             {
                 return 0;
-            } 
+            }
             else if (threads >= Environment.ProcessorCount)
             {
                 return Environment.ProcessorCount;
@@ -3080,7 +3087,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                         {
                             inputModifier += " -deint 1";
 
-                            if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.RealFrameRate ?? 60) > 30)
+                            if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.AverageFrameRate ?? 60) > 30)
                             {
                                 inputModifier += " -drop_second_field 1";
                             }
@@ -3864,7 +3871,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 GetInputArgument(state, encodingOptions),
                 threads,
                 " -vn",
-                string.Join(" ", audioTranscodeParams),
+                string.Join(' ', audioTranscodeParams),
                 outputPath,
                 string.Empty,
                 string.Empty,

+ 1 - 1
MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs

@@ -214,7 +214,7 @@ namespace MediaBrowser.LocalMetadata.Savers
 
             if (item.LockedFields.Length > 0)
             {
-                writer.WriteElementString("LockedFields", string.Join("|", item.LockedFields));
+                writer.WriteElementString("LockedFields", string.Join('|', item.LockedFields));
             }
 
             if (item.CriticRating.HasValue)

+ 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" />

+ 5 - 5
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))
             {
@@ -1496,7 +1496,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                             video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0], CultureInfo.InvariantCulture);
                             int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1], CultureInfo.InvariantCulture);
 
-                            description = string.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
+                            description = string.Join(' ', numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
                         }
                         else
                         {
@@ -1508,7 +1508,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                         if (subtitle.Contains('.', StringComparison.Ordinal))
                         {
                             // skip the comment, keep the subtitle
-                            description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+                            description = string.Join('.', subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
                         }
                         else
                         {

+ 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>();
-        }
     }
 }

+ 1 - 8
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -254,7 +254,7 @@ namespace MediaBrowser.Model.Configuration
         /// Gets or sets the preferred metadata language.
         /// </summary>
         /// <value>The preferred metadata language.</value>
-        public string PreferredMetadataLanguage { get; set; } = string.Empty;
+        public string PreferredMetadataLanguage { get; set; } = "en";
 
         /// <summary>
         /// Gets or sets the metadata country code.
@@ -418,8 +418,6 @@ namespace MediaBrowser.Model.Configuration
 
         public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
 
-        public bool EnableSimpleArtistDetection { get; set; } = false;
-
         public string[] UninstalledPlugins { get; set; } = Array.Empty<string>();
 
         /// <summary>
@@ -461,10 +459,5 @@ namespace MediaBrowser.Model.Configuration
         /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
         /// </summary>
         public bool RemoveOldPlugins { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether plugin image should be disabled.
-        /// </summary>
-        public bool DisablePluginImages { get; set; }
     }
 }

+ 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;
     }
 }

+ 9 - 7
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;
             }
 
@@ -1733,7 +1735,7 @@ namespace MediaBrowser.Model.Dlna
 
                                 if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
                                 {
-                                    item.SetOption(qualifier, "profile", string.Join(",", values));
+                                    item.SetOption(qualifier, "profile", string.Join(',', values));
                                 }
                             }
 

+ 576 - 596
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");
-
-                list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+                return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
             }
-
-            string queryString = string.Join("&", list.ToArray());
-
-            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)
+        /// <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;
+            }
+        }
+
+        /// <summary>
+        /// Gets the audio sample rate that will be in the output stream.
+        /// </summary>
+        public string TargetVideoProfile
+        {
+            get
+            {
+                if (IsDirectStream)
                 {
-                    list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+                    return TargetVideoStream == null ? null : TargetVideoStream.Profile;
                 }
 
-                if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+                var targetVideoCodecs = TargetVideoCodec;
+                var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+                if (!string.IsNullOrEmpty(videoCodec))
                 {
-                    list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+                    return GetOption(videoCodec, "profile");
                 }
 
-                if (item.CopyTimestamps)
-                {
-                    list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
-                }
-
-                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(" ", "")));
+                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
+            string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+
+            baseUrl = baseUrl.TrimEnd('/');
+
+            if (MediaType == DlnaProfileType.Audio)
             {
-                if (IsDirectStream)
+                if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
                 {
-                    return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+                    return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
                 }
 
-                var targetAudioCodecs = TargetAudioCodec;
-                var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
-                if (!string.IsNullOrEmpty(codec))
-                {
-                    return GetTargetRefFrames(codec);
-                }
+                return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+            }
 
-                return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+            if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+            {
+                return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
             }
+
+            return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
         }
 
-        public int? GetTargetAudioChannels(string codec)
+        private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
         {
-            var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
-
-            var value = GetOption(codec, "audiochannels");
-            if (string.IsNullOrEmpty(value))
-            {
-                return defaultValue;
-            }
+            var list = new List<NameValuePair>();
 
-            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+            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)
             {
-                return Math.Min(result, defaultValue ?? result);
+                list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+            }
+            else
+            {
+                list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
             }
 
-            return defaultValue;
-        }
+            list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+            list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
 
-        /// <summary>
-        /// Predicts the audio codec that will be in the output stream.
-        /// </summary>
-        public string[] TargetAudioCodec
-        {
-            get
-            {
-                var stream = TargetAudioStream;
+            string liveStreamId = item.MediaSource?.LiveStreamId;
+            list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
 
-                string inputCodec = stream?.Codec;
+            list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
 
-                if (IsDirectStream)
+            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";
-    }
 }

+ 99 - 100
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))
@@ -211,7 +211,7 @@ namespace MediaBrowser.Model.Entities
                             return result.ToString();
                         }
 
-                        return string.Join(" ", attributes);
+                        return string.Join(' ', attributes);
                     }
 
                     case MediaStreamType.Subtitle:
@@ -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>();
-        }
     }
 }

Vissa filer visades inte eftersom för många filer har ändrats