Browse Source

Merge branch 'master' into event-rewrite-1

Patrick Barron 4 years ago
parent
commit
98ed90c4a2
74 changed files with 2340 additions and 2267 deletions
  1. 6 0
      Emby.Dlna/ControlResponse.cs
  2. 3 0
      Emby.Server.Implementations/ApplicationHost.cs
  3. 5 43
      Emby.Server.Implementations/Dto/DtoService.cs
  4. 4 5
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  5. 17 17
      Emby.Server.Implementations/Localization/Core/bn.json
  6. 36 14
      Emby.Server.Implementations/Localization/Core/id.json
  7. 2 2
      Emby.Server.Implementations/Localization/Core/it.json
  8. 18 2
      Emby.Server.Implementations/Localization/Core/ta.json
  9. 6 161
      Jellyfin.Api/Controllers/AudioController.cs
  10. 1 1
      Jellyfin.Api/Controllers/DevicesController.cs
  11. 6 6
      Jellyfin.Api/Controllers/DlnaServerController.cs
  12. 8 440
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  13. 2 2
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  14. 3 3
      Jellyfin.Api/Controllers/ItemLookupController.cs
  15. 14 14
      Jellyfin.Api/Controllers/LiveTvController.cs
  16. 38 500
      Jellyfin.Api/Controllers/MediaInfoController.cs
  17. 0 1
      Jellyfin.Api/Controllers/NotificationsController.cs
  18. 1 1
      Jellyfin.Api/Controllers/PlaystateController.cs
  19. 10 18
      Jellyfin.Api/Controllers/RemoteImageController.cs
  20. 2 2
      Jellyfin.Api/Controllers/SessionController.cs
  21. 141 140
      Jellyfin.Api/Controllers/UniversalAudioController.cs
  22. 4 5
      Jellyfin.Api/Controllers/VideosController.cs
  23. 195 0
      Jellyfin.Api/Helpers/AudioHelper.cs
  24. 550 0
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  25. 19 20
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  26. 573 0
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  27. 44 0
      Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
  28. 1 0
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  29. 31 0
      Jellyfin.Server/Formatters/XmlOutputFormatter.cs
  30. 6 1
      Jellyfin.Server/Startup.cs
  31. 3 3
      MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
  32. 0 24
      MediaBrowser.Controller/Dto/IDtoService.cs
  33. 2 1
      MediaBrowser.Controller/Providers/IProviderManager.cs
  34. 2 1
      MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
  35. 2 1
      MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
  36. 0 29
      MediaBrowser.Model/Extensions/ListHelper.cs
  37. 20 6
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  38. 21 15
      MediaBrowser.Providers/Manager/ProviderManager.cs
  39. 2 1
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  40. 0 1
      MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
  41. 7 10
      MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
  42. 8 17
      MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
  43. 7 10
      MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
  44. 10 21
      MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
  45. 94 119
      MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
  46. 9 15
      MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
  47. 7 7
      MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
  48. 7 11
      MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
  49. 61 69
      MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
  50. 15 30
      MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
  51. 6 10
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
  52. 6 10
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
  53. 6 10
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
  54. 6 10
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
  55. 6 10
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
  56. 6 11
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
  57. 6 10
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
  58. 20 32
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
  59. 6 10
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
  60. 36 71
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
  61. 57 73
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
  62. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
  63. 6 10
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
  64. 25 38
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  65. 4 4
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
  66. 4 4
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
  67. 14 20
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
  68. 6 10
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
  69. 14 20
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
  70. 6 10
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
  71. 53 64
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
  72. 2 1
      MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
  73. 6 10
      MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
  74. 14 28
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs

+ 6 - 0
Emby.Dlna/ControlResponse.cs

@@ -16,5 +16,11 @@ namespace Emby.Dlna
         public string Xml { get; set; }
 
         public bool IsSuccessful { get; set; }
+
+        /// <inheritdoc />
+        public override string ToString()
+        {
+            return Xml;
+        }
     }
 }

+ 3 - 0
Emby.Server.Implementations/ApplicationHost.cs

@@ -635,6 +635,9 @@ namespace Emby.Server.Implementations
             ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
             ServiceCollection.AddSingleton<TranscodingJobHelper>();
+            ServiceCollection.AddScoped<MediaInfoHelper>();
+            ServiceCollection.AddScoped<AudioHelper>();
+            ServiceCollection.AddScoped<DynamicHlsHelper>();
         }
 
         /// <summary>

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

@@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
             _livetvManagerFactory = livetvManagerFactory;
         }
 
-        /// <summary>
-        /// Converts a BaseItem to a DTOBaseItem.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="fields">The fields.</param>
-        /// <param name="user">The user.</param>
-        /// <param name="owner">The owner.</param>
-        /// <returns>Task{DtoBaseItem}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
-        {
-            var options = new DtoOptions
-            {
-                Fields = fields
-            };
-
-            return GetBaseItemDto(item, options, user, owner);
-        }
-
         /// <inheritdoc />
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         {
@@ -443,17 +424,6 @@ namespace Emby.Server.Implementations.Dto
             return folder.GetChildCount(user);
         }
 
-        /// <summary>
-        /// Gets client-side Id of a server-side BaseItem.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public string GetDtoId(BaseItem item)
-        {
-            return item.Id.ToString("N", CultureInfo.InvariantCulture);
-        }
-
         private static void SetBookProperties(BaseItemDto dto, Book item)
         {
             dto.SeriesName = item.SeriesName;
@@ -484,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
             }
         }
 
+        private string GetDtoId(BaseItem item)
+        {
+            return item.Id.ToString("N", CultureInfo.InvariantCulture);
+        }
+
         private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
         {
             if (!string.IsNullOrEmpty(item.Album))
@@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
                 .ToArray();
         }
 
-        private string GetImageCacheTag(BaseItem item, ImageType type)
-        {
-            try
-            {
-                return _imageProcessor.GetImageCacheTag(item, type);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error getting {type} image info", type);
-                return null;
-            }
-        }
-
         private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         {
             try

+ 4 - 5
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     {
         // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
-        private const char StreamIdDelimeter = '_';
-        private const string StreamIdDelimeterString = "_";
+        private const char StreamIdDelimiter = '_';
 
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILogger<LiveTvMediaSourceProvider> _logger;
@@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv
                 }
             }
 
-            return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
+            return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
         }
 
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
@@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
                         source.Id ?? string.Empty
                     };
 
-                    source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
+                    source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
                 }
 
                 // Dummy this up so that direct play checks can still run
@@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
         /// <inheritdoc />
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         {
-            var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
+            var keys = openToken.Split(StreamIdDelimiter, 3);
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
 
             var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);

+ 17 - 17
Emby.Server.Implementations/Localization/Core/bn.json

@@ -1,12 +1,12 @@
 {
     "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
     "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
-    "Collections": "সংকলন",
+    "Collections": "কলেক্শন",
     "ChapterNameValue": "অধ্যায় {0}",
     "Channels": "চ্যানেল",
-    "CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
+    "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
     "Books": "বই",
-    "AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
+    "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
     "Artists": "শিল্পীরা",
     "Application": "অ্যাপ্লিকেশন",
     "Albums": "অ্যালবামগুলো",
@@ -14,13 +14,13 @@
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
-    "HeaderCameraUploads": "ক্যামেরার আপলোডগুলো",
-    "HeaderAlbumArtists": "এলবামের শিল্পী",
-    "Genres": "ঘরানা",
+    "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
+    "HeaderAlbumArtists": "এলবাম শিল্পী",
+    "Genres": "জেনার",
     "Folders": "ফোল্ডারগুলো",
-    "Favorites": "ফেভারিটগুলো",
+    "Favorites": "পছন্দসমূহ",
     "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
-    "AppDeviceValues": "প: {0}, ডিভাইস: {0}",
+    "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
     "VersionNumber": "সংস্করণ {0}",
     "ValueSpecialEpisodeName": "বিশেষ - {0}",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
@@ -74,20 +74,20 @@
     "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
     "MusicVideos": "গানের ভিডিও",
     "Music": "গান",
-    "Movies": "সিনেমা",
+    "Movies": "চলচ্চিত্র",
     "MixedContent": "মিশ্র কন্টেন্ট",
-    "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে",
-    "HeaderRecordingGroups": "রেকর্ডিং গ্রুপ",
-    "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে",
-    "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে",
-    "MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে",
-    "Latest": "একদম নতুন",
+    "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
+    "HeaderRecordingGroups": "রেকর্ডিং দল",
+    "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
+    "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
+    "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
+    "Latest": "সর্বশেষ",
     "LabelRunningTimeValue": "চলার সময়: {0}",
-    "LabelIpAddressValue": "আইপি ঠিকানা: {0}",
+    "LabelIpAddressValue": "আইপি এড্রেস: {0}",
     "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
     "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
     "Inherit": "থেকে পাওয়া",
-    "HomeVideos": "বাসার ভিডিও",
+    "HomeVideos": "হোম ভিডিও",
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",

+ 36 - 14
Emby.Server.Implementations/Localization/Core/id.json

@@ -7,8 +7,8 @@
     "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
     "Latest": "Terbaru",
     "LabelIpAddressValue": "Alamat IP: {0}",
-    "ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan",
-    "ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
+    "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
+    "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
     "Inherit": "Warisan",
     "HomeVideos": "Video Rumah",
     "HeaderRecordingGroups": "Grup Rekaman",
@@ -19,8 +19,8 @@
     "HeaderFavoriteEpisodes": "Episode Favorit",
     "HeaderFavoriteArtists": "Artis Favorit",
     "HeaderFavoriteAlbums": "Album Favorit",
-    "HeaderContinueWatching": "Masih Melihat",
-    "HeaderCameraUploads": "Uplod Kamera",
+    "HeaderContinueWatching": "Lanjutkan Menonton",
+    "HeaderCameraUploads": "Unggahan Kamera",
     "HeaderAlbumArtists": "Album Artis",
     "Genres": "Genre",
     "Folders": "Folder",
@@ -32,11 +32,11 @@
     "ChapterNameValue": "Bagian {0}",
     "Channels": "Saluran",
     "TvShows": "Seri TV",
-    "SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
-    "StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
+    "SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
+    "StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
     "Songs": "Lagu",
     "Playlists": "Daftar putar",
-    "NotificationOptionPluginUninstalled": "Plugin dilepas",
+    "NotificationOptionPluginUninstalled": "Plugin dihapus",
     "MusicVideos": "Video musik",
     "VersionNumber": "Versi {0}",
     "ValueSpecialEpisodeName": "Spesial - {0}",
@@ -65,7 +65,7 @@
     "Photos": "Foto",
     "NotificationOptionUserLockedOut": "Pengguna terkunci",
     "NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
-    "NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
+    "NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
     "NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
     "NotificationOptionPluginInstalled": "Plugin terpasang",
     "NotificationOptionPluginError": "Kegagalan plugin",
@@ -74,14 +74,14 @@
     "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
     "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
     "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
-    "NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.",
+    "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
     "NameSeasonUnknown": "Musim tak diketahui",
     "NameSeasonNumber": "Musim {0}",
-    "NameInstallFailed": "{0} instalasi gagal",
+    "NameInstallFailed": "{0} penginstalan gagal",
     "Music": "Musik",
     "Movies": "Film",
-    "MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
+    "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
     "FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
     "CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
     "DeviceOfflineWithName": "{0} telah terputus",
@@ -90,6 +90,28 @@
     "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
     "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
     "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
-    "MixedContent": "Konten campur",
-    "PluginUninstalledWithName": "{0} telah dihapus"
+    "MixedContent": "Konten campuran",
+    "PluginUninstalledWithName": "{0} telah dihapus",
+    "TaskRefreshChapterImagesDescription": "Membuat gambar mini untuk video yang memiliki bagian.",
+    "TaskRefreshChapterImages": "Ekstrak Gambar Bagian",
+    "TaskCleanCacheDescription": "Menghapus file cache yang tidak lagi dibutuhkan oleh sistem.",
+    "TaskCleanCache": "Bersihkan Cache Direktori",
+    "TasksLibraryCategory": "Pustaka",
+    "TasksMaintenanceCategory": "Perbaikan",
+    "TasksApplicationCategory": "Aplikasi",
+    "TaskRefreshPeopleDescription": "Memperbarui metadata untuk aktor dan sutradara di pustaka media Anda.",
+    "TaskRefreshLibraryDescription": "Memindai Pustaka media Anda untuk mencari file baru dan memperbarui metadata.",
+    "TasksChannelsCategory": "Saluran Online",
+    "TaskDownloadMissingSubtitlesDescription": "Mencari di internet untuk subtitle yang hilang berdasarkan konfigurasi metadata.",
+    "TaskDownloadMissingSubtitles": "Unduh subtitle yang hilang",
+    "TaskRefreshChannelsDescription": "Segarkan informasi saluran internet.",
+    "TaskRefreshChannels": "Segarkan Saluran",
+    "TaskCleanTranscodeDescription": "Menghapus file transcode yang berumur lebih dari satu hari.",
+    "TaskCleanTranscode": "Bersihkan Direktori Transcode",
+    "TaskUpdatePluginsDescription": "Unduh dan instal pembaruan untuk plugin yang dikonfigurasi untuk memperbarui secara otomatis.",
+    "TaskUpdatePlugins": "Perbarui Plugin",
+    "TaskRefreshPeople": "Muat ulang Orang",
+    "TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
+    "TaskCleanLogs": "Bersihkan Log Direktori",
+    "TaskRefreshLibrary": "Pindai Pustaka Media"
 }

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

@@ -102,11 +102,11 @@
     "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
     "TaskUpdatePlugins": "Aggiorna i Plugin",
     "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
-    "TaskRefreshPeople": "Aggiorna persone",
+    "TaskRefreshPeople": "Aggiornamento Persone",
     "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
     "TaskCleanLogs": "Pulisci la cartella dei log",
     "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
-    "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali",
+    "TaskRefreshLibrary": "Scan Librerie",
     "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
     "TaskRefreshChapterImages": "Estrai immagini capitolo",
     "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",

+ 18 - 2
Emby.Server.Implementations/Localization/Core/ta.json

@@ -45,7 +45,7 @@
     "TvShows": "தொலைக்காட்சித் தொடர்கள்",
     "Sync": "ஒத்திசைவு",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
-    "Songs": "பாட்டுகள்",
+    "Songs": "பாட்கள்",
     "Shows": "தொடர்கள்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
@@ -97,5 +97,21 @@
     "Application": "செயலி",
     "Albums": "ஆல்பங்கள்",
     "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
-    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது"
+    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+    "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
+    "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
+    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+    "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+    "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+    "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
+    "TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்",
+    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+    "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
+    "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
+    "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
+    "HomeVideos": "முகப்பு வீடியோக்கள்",
+    "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+    "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
 }

+ 6 - 161
Jellyfin.Api/Controllers/AudioController.cs

@@ -1,93 +1,32 @@
 using System;
 using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.StreamingDtos;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
 
 namespace Jellyfin.Api.Controllers
 {
     /// <summary>
     /// The audio controller.
     /// </summary>
-    // TODO: In order to autheneticate this in the future, Dlna playback will require updating
+    // TODO: In order to authenticate this in the future, Dlna playback will require updating
     public class AudioController : BaseJellyfinApiController
     {
-        private readonly IDlnaManager _dlnaManager;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IFileSystem _fileSystem;
-        private readonly ISubtitleEncoder _subtitleEncoder;
-        private readonly IConfiguration _configuration;
-        private readonly IDeviceManager _deviceManager;
-        private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly AudioHelper _audioHelper;
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioController"/> class.
         /// </summary>
-        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
-        /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
-        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
-        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
-        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
-        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
-        /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
-        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
-        public AudioController(
-            IDlnaManager dlnaManager,
-            IUserManager userManger,
-            IAuthorizationContext authorizationContext,
-            ILibraryManager libraryManager,
-            IMediaSourceManager mediaSourceManager,
-            IServerConfigurationManager serverConfigurationManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            ISubtitleEncoder subtitleEncoder,
-            IConfiguration configuration,
-            IDeviceManager deviceManager,
-            TranscodingJobHelper transcodingJobHelper,
-            IHttpClientFactory httpClientFactory)
+        /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+        public AudioController(AudioHelper audioHelper)
         {
-            _dlnaManager = dlnaManager;
-            _authContext = authorizationContext;
-            _userManager = userManger;
-            _libraryManager = libraryManager;
-            _mediaSourceManager = mediaSourceManager;
-            _serverConfigurationManager = serverConfigurationManager;
-            _mediaEncoder = mediaEncoder;
-            _fileSystem = fileSystem;
-            _subtitleEncoder = subtitleEncoder;
-            _configuration = configuration;
-            _deviceManager = deviceManager;
-            _transcodingJobHelper = transcodingJobHelper;
-            _httpClientFactory = httpClientFactory;
+            _audioHelper = audioHelper;
         }
 
         /// <summary>
@@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string>? streamOptions)
         {
-            bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-
-            var cancellationTokenSource = new CancellationTokenSource();
-
             StreamingRequestDto streamingRequest = new StreamingRequestDto
             {
                 Id = itemId,
@@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers
                 StreamOptions = streamOptions
             };
 
-            using var state = await StreamingHelpers.GetStreamingState(
-                    streamingRequest,
-                    Request,
-                    _authContext,
-                    _mediaSourceManager,
-                    _userManager,
-                    _libraryManager,
-                    _serverConfigurationManager,
-                    _mediaEncoder,
-                    _fileSystem,
-                    _subtitleEncoder,
-                    _configuration,
-                    _dlnaManager,
-                    _deviceManager,
-                    _transcodingJobHelper,
-                    _transcodingJobType,
-                    cancellationTokenSource.Token)
-                .ConfigureAwait(false);
-
-            if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
-            {
-                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
-
-                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
-                    {
-                        AllowEndOfFile = false
-                    }.WriteToAsync(Response.Body, CancellationToken.None)
-                    .ConfigureAwait(false);
-
-                // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
-                return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
-            }
-
-            // Static remote stream
-            if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
-            {
-                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
-
-                using var httpClient = _httpClientFactory.CreateClient();
-                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
-            }
-
-            if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
-            {
-                return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
-            }
-
-            var outputPath = state.OutputFilePath;
-            var outputPathExists = System.IO.File.Exists(outputPath);
-
-            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
-            var isTranscodeCached = outputPathExists && transcodingJob != null;
-
-            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
-
-            // Static stream
-            if (@static.HasValue && @static.Value)
-            {
-                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
-
-                if (state.MediaSource.IsInfiniteStream)
-                {
-                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
-                        {
-                            AllowEndOfFile = false
-                        }.WriteToAsync(Response.Body, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-                    return File(Response.Body, contentType);
-                }
-
-                return FileStreamResponseHelpers.GetStaticFileResult(
-                    state.MediaPath,
-                    contentType,
-                    isHeadRequest,
-                    this);
-            }
-
-            // Need to start ffmpeg (because media can't be returned directly)
-            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
-            var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
-            var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
-            return await FileStreamResponseHelpers.GetTranscodedFile(
-                state,
-                isHeadRequest,
-                this,
-                _transcodingJobHelper,
-                ffmpegCommandLineArguments,
-                Request,
-                _transcodingJobType,
-                cancellationTokenSource).ConfigureAwait(false);
+            return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
         }
     }
 }

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

@@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
+        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
         {
             var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
             return _deviceManager.GetDevices(deviceQuery);

+ 6 - 6
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -60,8 +60,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <response code="200">Dlna content directory returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
-        [HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
-        [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")]
+        [HttpGet("{serverId}/ContentDirectory")]
+        [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
-        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
-        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@@ -90,8 +90,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
-        [HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
-        [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")]
+        [HttpGet("{serverId}/ConnectionManager")]
+        [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]

+ 8 - 440
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
@@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Authorization;
@@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly INetworkManager _networkManager;
         private readonly ILogger<DynamicHlsController> _logger;
         private readonly EncodingHelper _encodingHelper;
+        private readonly DynamicHlsHelper _dynamicHlsHelper;
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
 
@@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
+        /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
         public DynamicHlsController(
             ILibraryManager libraryManager,
             IUserManager userManager,
@@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers
             IConfiguration configuration,
             IDeviceManager deviceManager,
             TranscodingJobHelper transcodingJobHelper,
-            INetworkManager networkManager,
-            ILogger<DynamicHlsController> logger)
+            ILogger<DynamicHlsController> logger,
+            DynamicHlsHelper dynamicHlsHelper)
         {
             _libraryManager = libraryManager;
             _userManager = userManager;
@@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers
             _configuration = configuration;
             _deviceManager = deviceManager;
             _transcodingJobHelper = transcodingJobHelper;
-            _networkManager = networkManager;
             _logger = logger;
+            _dynamicHlsHelper = dynamicHlsHelper;
 
             _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
         }
@@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
         {
-            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-            var cancellationTokenSource = new CancellationTokenSource();
             var streamingRequest = new HlsVideoRequestDto
             {
                 Id = itemId,
@@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
             };
 
-            return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
-                .ConfigureAwait(false);
+            return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
         {
-            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-            var cancellationTokenSource = new CancellationTokenSource();
             var streamingRequest = new HlsAudioRequestDto
             {
                 Id = itemId,
@@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
             };
 
-            return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
-                .ConfigureAwait(false);
+            return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers
                 .ConfigureAwait(false);
         }
 
-        private async Task<ActionResult> GetMasterPlaylistInternal(
-            StreamingRequestDto streamingRequest,
-            bool isHeadRequest,
-            bool enableAdaptiveBitrateStreaming,
-            CancellationTokenSource cancellationTokenSource)
-        {
-            using var state = await StreamingHelpers.GetStreamingState(
-                    streamingRequest,
-                    Request,
-                    _authContext,
-                    _mediaSourceManager,
-                    _userManager,
-                    _libraryManager,
-                    _serverConfigurationManager,
-                    _mediaEncoder,
-                    _fileSystem,
-                    _subtitleEncoder,
-                    _configuration,
-                    _dlnaManager,
-                    _deviceManager,
-                    _transcodingJobHelper,
-                    _transcodingJobType,
-                    cancellationTokenSource.Token)
-                .ConfigureAwait(false);
-
-            Response.Headers.Add(HeaderNames.Expires, "0");
-            if (isHeadRequest)
-            {
-                return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
-            }
-
-            var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
-
-            var builder = new StringBuilder();
-
-            builder.AppendLine("#EXTM3U");
-
-            var isLiveStream = state.IsSegmentedLiveStream;
-
-            var queryString = Request.QueryString.ToString();
-
-            // from universal audio service
-            if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
-            {
-                queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
-            }
-
-            // from universal audio service
-            if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
-            {
-                queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
-            }
-
-            // Main stream
-            var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
-
-            playlistUrl += queryString;
-
-            var subtitleStreams = state.MediaSource
-                .MediaStreams
-                .Where(i => i.IsTextSubtitleStream)
-                .ToList();
-
-            var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
-                ? "subs"
-                : null;
-
-            // If we're burning in subtitles then don't add additional subs to the manifest
-            if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
-            {
-                subtitleGroup = null;
-            }
-
-            if (!string.IsNullOrWhiteSpace(subtitleGroup))
-            {
-                AddSubtitles(state, subtitleStreams, builder);
-            }
-
-            AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
-            if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming))
-            {
-                var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
-                // By default, vary by just 200k
-                var variation = GetBitrateVariation(totalBitrate);
-
-                var newBitrate = totalBitrate - variation;
-                var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
-                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-
-                variation *= 2;
-                newBitrate = totalBitrate - variation;
-                variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
-                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-            }
-
-            return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
-        }
-
         private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
         {
             using var state = await StreamingHelpers.GetStreamingState(
@@ -1411,330 +1303,6 @@ namespace Jellyfin.Api.Controllers
             return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
         }
 
-        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
-        {
-            var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
-            const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
-
-            foreach (var stream in subtitles)
-            {
-                var name = stream.DisplayTitle;
-
-                var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
-                var isForced = stream.IsForced;
-
-                var url = string.Format(
-                    CultureInfo.InvariantCulture,
-                    "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
-                    state.Request.MediaSourceId,
-                    stream.Index.ToString(CultureInfo.InvariantCulture),
-                    30.ToString(CultureInfo.InvariantCulture),
-                    ClaimHelpers.GetToken(Request.HttpContext.User));
-
-                var line = string.Format(
-                    CultureInfo.InvariantCulture,
-                    Format,
-                    name,
-                    isDefault ? "YES" : "NO",
-                    isForced ? "YES" : "NO",
-                    url,
-                    stream.Language ?? "Unknown");
-
-                builder.AppendLine(line);
-            }
-        }
-
-        private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
-        {
-            builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
-                .Append(bitrate.ToString(CultureInfo.InvariantCulture))
-                .Append(",AVERAGE-BANDWIDTH=")
-                .Append(bitrate.ToString(CultureInfo.InvariantCulture));
-
-            AppendPlaylistCodecsField(builder, state);
-
-            AppendPlaylistResolutionField(builder, state);
-
-            AppendPlaylistFramerateField(builder, state);
-
-            if (!string.IsNullOrWhiteSpace(subtitleGroup))
-            {
-                builder.Append(",SUBTITLES=\"")
-                    .Append(subtitleGroup)
-                    .Append('"');
-            }
-
-            builder.Append(Environment.NewLine);
-            builder.AppendLine(url);
-        }
-
-        /// <summary>
-        /// Appends a CODECS field containing formatted strings of
-        /// the active streams output video and audio codecs.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
-        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
-        {
-            // Video
-            string videoCodecs = string.Empty;
-            int? videoCodecLevel = GetOutputVideoCodecLevel(state);
-            if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
-            {
-                videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
-            }
-
-            // Audio
-            string audioCodecs = string.Empty;
-            if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
-            {
-                audioCodecs = GetPlaylistAudioCodecs(state);
-            }
-
-            StringBuilder codecs = new StringBuilder();
-
-            codecs.Append(videoCodecs);
-
-            if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
-            {
-                codecs.Append(',');
-            }
-
-            codecs.Append(audioCodecs);
-
-            if (codecs.Length > 1)
-            {
-                builder.Append(",CODECS=\"")
-                    .Append(codecs)
-                    .Append('"');
-            }
-        }
-
-        /// <summary>
-        /// Appends a RESOLUTION field containing the resolution of the output stream.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
-        {
-            if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
-            {
-                builder.Append(",RESOLUTION=")
-                    .Append(state.OutputWidth.GetValueOrDefault())
-                    .Append('x')
-                    .Append(state.OutputHeight.GetValueOrDefault());
-            }
-        }
-
-        /// <summary>
-        /// Appends a FRAME-RATE field containing the framerate of the output stream.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
-        {
-            double? framerate = null;
-            if (state.TargetFramerate.HasValue)
-            {
-                framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
-            }
-            else if (state.VideoStream?.RealFrameRate != null)
-            {
-                framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
-            }
-
-            if (framerate.HasValue)
-            {
-                builder.Append(",FRAME-RATE=")
-                    .Append(framerate.Value);
-            }
-        }
-
-        private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming)
-        {
-            // Within the local network this will likely do more harm than good.
-            var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString();
-            if (_networkManager.IsInLocalNetwork(ip))
-            {
-                return false;
-            }
-
-            if (!enableAdaptiveBitrateStreaming)
-            {
-                return false;
-            }
-
-            if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
-            {
-                // Opening live streams is so slow it's not even worth it
-                return false;
-            }
-
-            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
-            {
-                return false;
-            }
-
-            if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
-            {
-                return false;
-            }
-
-            if (!state.IsOutputVideo)
-            {
-                return false;
-            }
-
-            // Having problems in android
-            return false;
-            // return state.VideoRequest.VideoBitRate.HasValue;
-        }
-
-        /// <summary>
-        /// Get the H.26X level of the output video stream.
-        /// </summary>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>H.26X level of the output video stream.</returns>
-        private int? GetOutputVideoCodecLevel(StreamState state)
-        {
-            string? levelString;
-            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
-                && state.VideoStream.Level.HasValue)
-            {
-                levelString = state.VideoStream?.Level.ToString();
-            }
-            else
-            {
-                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
-            }
-
-            if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
-            {
-                return parsedLevel;
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
-        /// </summary>
-        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
-        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>Formatted audio codec string.</returns>
-        private string GetPlaylistAudioCodecs(StreamState state)
-        {
-            if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
-            {
-                string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
-                return HlsCodecStringHelpers.GetAACString(profile);
-            }
-
-            if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
-            {
-                return HlsCodecStringHelpers.GetMP3String();
-            }
-
-            if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
-            {
-                return HlsCodecStringHelpers.GetAC3String();
-            }
-
-            if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
-            {
-                return HlsCodecStringHelpers.GetEAC3String();
-            }
-
-            return string.Empty;
-        }
-
-        /// <summary>
-        /// Gets a formatted string of the output video codec, for use in the CODECS field.
-        /// </summary>
-        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
-        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <param name="codec">Video codec.</param>
-        /// <param name="level">Video level.</param>
-        /// <returns>Formatted video codec string.</returns>
-        private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
-        {
-            if (level == 0)
-            {
-                // This is 0 when there's no requested H.26X level in the device profile
-                // and the source is not encoded in H.26X
-                _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
-                return string.Empty;
-            }
-
-            if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
-            {
-                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
-                return HlsCodecStringHelpers.GetH264String(profile, level);
-            }
-
-            if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
-            {
-                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
-                return HlsCodecStringHelpers.GetH265String(profile, level);
-            }
-
-            return string.Empty;
-        }
-
-        private int GetBitrateVariation(int bitrate)
-        {
-            // By default, vary by just 50k
-            var variation = 50000;
-
-            if (bitrate >= 10000000)
-            {
-                variation = 2000000;
-            }
-            else if (bitrate >= 5000000)
-            {
-                variation = 1500000;
-            }
-            else if (bitrate >= 3000000)
-            {
-                variation = 1000000;
-            }
-            else if (bitrate >= 2000000)
-            {
-                variation = 500000;
-            }
-            else if (bitrate >= 1000000)
-            {
-                variation = 300000;
-            }
-            else if (bitrate >= 600000)
-            {
-                variation = 200000;
-            }
-            else if (bitrate >= 400000)
-            {
-                variation = 100000;
-            }
-
-            return variation;
-        }
-
-        private string ReplaceBitrate(string url, int oldValue, int newValue)
-        {
-            return url.Replace(
-                "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
-                "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
-                StringComparison.OrdinalIgnoreCase);
-        }
-
         private double[] GetSegmentLengths(StreamState state)
         {
             var result = new List<double>();
@@ -2089,7 +1657,7 @@ namespace Jellyfin.Api.Controllers
                 return Task.CompletedTask;
             });
 
-            return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this);
+            return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
         }
 
         private long GetEndPositionTicks(StreamState state, int requestedIndex)

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

@@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
             var file = segmentId + Path.GetExtension(Request.Path);
             file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
 
-            return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this);
+            return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
         }
 
         /// <summary>
@@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers
                 return Task.CompletedTask;
             });
 
-            return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this);
+            return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
         }
     }
 }

+ 3 - 3
Jellyfin.Api/Controllers/ItemLookupController.cs

@@ -331,12 +331,12 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Task.</returns>
         private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
         {
-            var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
-            var ext = result.ContentType.Split('/').Last();
+            using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
+            var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
             Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            await using (var stream = result.Content)
+            using (var stream = result.Content)
             {
                 await using var fileStream = new FileStream(
                     fullCachePath,

+ 14 - 14
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Net.Mime;
 using System.Security.Cryptography;
 using System.Text;
@@ -15,7 +16,6 @@ using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly IUserManager _userManager;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
         private readonly ISessionContext _sessionContext;
@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
         /// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
@@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers
         public LiveTvController(
             ILiveTvManager liveTvManager,
             IUserManager userManager,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager,
             IDtoService dtoService,
             ISessionContext sessionContext,
@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers
         {
             _liveTvManager = liveTvManager;
             _userManager = userManager;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             _dtoService = dtoService;
             _sessionContext = sessionContext;
@@ -592,11 +592,11 @@ namespace Jellyfin.Api.Controllers
                 GenreIds = RequestHelpers.GetGuids(genreIds)
             };
 
-            if (!librarySeriesId.Equals(Guid.Empty))
+            if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
             {
                 query.IsSeries = true;
 
-                if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series)
+                if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
                 {
                     query.Name = series.Name;
                 }
@@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="validateLogin">Validate login.</param>
         /// <response code="200">Created listings provider returned.</response>
         /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
-        [HttpGet("ListingProviders")]
+        [HttpPost("ListingProviders")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
@@ -1069,13 +1069,13 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSchedulesDirectCountries()
         {
+            var client = _httpClientFactory.CreateClient();
             // https://json.schedulesdirect.org/20141201/available/countries
-            var response = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = "https://json.schedulesdirect.org/20141201/available/countries",
-                BufferContent = false
-            }).ConfigureAwait(false);
-            return File(response, MediaTypeNames.Application.Json);
+            // Can't dispose the response as it's required up the call chain.
+            var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")
+                .ConfigureAwait(false);
+
+            return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
         }
 
         /// <summary>

+ 38 - 500
Jellyfin.Api/Controllers/MediaInfoController.cs

@@ -1,30 +1,18 @@
 using System;
 using System.Buffers;
 using System.ComponentModel.DataAnnotations;
-using System.Globalization;
 using System.Linq;
 using System.Net.Mime;
-using System.Text.Json;
-using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.MediaInfoDtos;
 using Jellyfin.Api.Models.VideoDtos;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IDeviceManager _deviceManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly INetworkManager _networkManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IUserManager _userManager;
         private readonly IAuthorizationContext _authContext;
         private readonly ILogger<MediaInfoController> _logger;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly MediaInfoHelper _mediaInfoHelper;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@@ -55,32 +40,23 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
-        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
         public MediaInfoController(
             IMediaSourceManager mediaSourceManager,
             IDeviceManager deviceManager,
             ILibraryManager libraryManager,
-            INetworkManager networkManager,
-            IMediaEncoder mediaEncoder,
-            IUserManager userManager,
             IAuthorizationContext authContext,
             ILogger<MediaInfoController> logger,
-            IServerConfigurationManager serverConfigurationManager)
+            MediaInfoHelper mediaInfoHelper)
         {
             _mediaSourceManager = mediaSourceManager;
             _deviceManager = deviceManager;
             _libraryManager = libraryManager;
-            _networkManager = networkManager;
-            _mediaEncoder = mediaEncoder;
-            _userManager = userManager;
             _authContext = authContext;
             _logger = logger;
-            _serverConfigurationManager = serverConfigurationManager;
+            _mediaInfoHelper = mediaInfoHelper;
         }
 
         /// <summary>
@@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
         {
-            return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
+            return await _mediaInfoHelper.GetPlaybackInfo(
+                    itemId,
+                    userId)
+                .ConfigureAwait(false);
         }
 
         /// <summary>
@@ -153,7 +132,12 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
+            var info = await _mediaInfoHelper.GetPlaybackInfo(
+                    itemId,
+                    userId,
+                    mediaSourceId,
+                    liveStreamId)
+                .ConfigureAwait(false);
 
             if (profile != null)
             {
@@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers
 
                 foreach (var mediaSource in info.MediaSources)
                 {
-                    SetDeviceSpecificData(
+                    _mediaInfoHelper.SetDeviceSpecificData(
                         item,
                         mediaSource,
                         profile,
@@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers
                         enableDirectStream,
                         enableTranscoding,
                         allowVideoStreamCopy,
-                        allowAudioStreamCopy);
+                        allowAudioStreamCopy,
+                        Request.HttpContext.Connection.RemoteIpAddress.ToString());
                 }
 
-                SortMediaSources(info, maxStreamingBitrate);
+                _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
             }
 
             if (autoOpenLiveStream)
@@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers
 
                 if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
                 {
-                    var openStreamResult = await OpenMediaSource(new LiveStreamRequest
-                    {
-                        AudioStreamIndex = audioStreamIndex,
-                        DeviceProfile = deviceProfile?.DeviceProfile,
-                        EnableDirectPlay = enableDirectPlay,
-                        EnableDirectStream = enableDirectStream,
-                        ItemId = itemId,
-                        MaxAudioChannels = maxAudioChannels,
-                        MaxStreamingBitrate = maxStreamingBitrate,
-                        PlaySessionId = info.PlaySessionId,
-                        StartTimeTicks = startTimeTicks,
-                        SubtitleStreamIndex = subtitleStreamIndex,
-                        UserId = userId ?? Guid.Empty,
-                        OpenToken = mediaSource.OpenToken
-                    }).ConfigureAwait(false);
+                    var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
+                        Request,
+                        new LiveStreamRequest
+                        {
+                            AudioStreamIndex = audioStreamIndex,
+                            DeviceProfile = deviceProfile?.DeviceProfile,
+                            EnableDirectPlay = enableDirectPlay,
+                            EnableDirectStream = enableDirectStream,
+                            ItemId = itemId,
+                            MaxAudioChannels = maxAudioChannels,
+                            MaxStreamingBitrate = maxStreamingBitrate,
+                            PlaySessionId = info.PlaySessionId,
+                            StartTimeTicks = startTimeTicks,
+                            SubtitleStreamIndex = subtitleStreamIndex,
+                            UserId = userId ?? Guid.Empty,
+                            OpenToken = mediaSource.OpenToken
+                        }).ConfigureAwait(false);
 
                     info.MediaSources = new[] { openStreamResult.MediaSource };
                 }
@@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers
             {
                 foreach (var mediaSource in info.MediaSources)
                 {
-                    NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
+                    _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
                 }
             }
 
@@ -271,7 +258,7 @@ namespace Jellyfin.Api.Controllers
                 EnableDirectStream = enableDirectStream,
                 DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
             };
-            return await OpenMediaSource(request).ConfigureAwait(false);
+            return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers
                 ArrayPool<byte>.Shared.Return(buffer);
             }
         }
-
-        private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
-            Guid id,
-            Guid? userId,
-            string? mediaSourceId = null,
-            string? liveStreamId = null)
-        {
-            var user = userId.HasValue && !userId.Equals(Guid.Empty)
-                ? _userManager.GetUserById(userId.Value)
-                : null;
-            var item = _libraryManager.GetItemById(id);
-            var result = new PlaybackInfoResponse();
-
-            MediaSourceInfo[] mediaSources;
-            if (string.IsNullOrWhiteSpace(liveStreamId))
-            {
-                // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
-                var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
-                if (string.IsNullOrWhiteSpace(mediaSourceId))
-                {
-                    mediaSources = mediaSourcesList.ToArray();
-                }
-                else
-                {
-                    mediaSources = mediaSourcesList
-                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
-                        .ToArray();
-                }
-            }
-            else
-            {
-                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
-
-                mediaSources = new[] { mediaSource };
-            }
-
-            if (mediaSources.Length == 0)
-            {
-                result.MediaSources = Array.Empty<MediaSourceInfo>();
-
-                result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
-            }
-            else
-            {
-                // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
-                // Should we move this directly into MediaSourceManager?
-                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
-
-                result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-            }
-
-            return result;
-        }
-
-        private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
-        {
-            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
-        }
-
-        private void SetDeviceSpecificData(
-            BaseItem item,
-            MediaSourceInfo mediaSource,
-            DeviceProfile profile,
-            AuthorizationInfo auth,
-            long? maxBitrate,
-            long startTimeTicks,
-            string mediaSourceId,
-            int? audioStreamIndex,
-            int? subtitleStreamIndex,
-            int? maxAudioChannels,
-            string playSessionId,
-            Guid userId,
-            bool enableDirectPlay,
-            bool enableDirectStream,
-            bool enableTranscoding,
-            bool allowVideoStreamCopy,
-            bool allowAudioStreamCopy)
-        {
-            var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
-
-            var options = new VideoOptions
-            {
-                MediaSources = new[] { mediaSource },
-                Context = EncodingContext.Streaming,
-                DeviceId = auth.DeviceId,
-                ItemId = item.Id,
-                Profile = profile,
-                MaxAudioChannels = maxAudioChannels
-            };
-
-            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
-            {
-                options.MediaSourceId = mediaSourceId;
-                options.AudioStreamIndex = audioStreamIndex;
-                options.SubtitleStreamIndex = subtitleStreamIndex;
-            }
-
-            var user = _userManager.GetUserById(userId);
-
-            if (!enableDirectPlay)
-            {
-                mediaSource.SupportsDirectPlay = false;
-            }
-
-            if (!enableDirectStream)
-            {
-                mediaSource.SupportsDirectStream = false;
-            }
-
-            if (!enableTranscoding)
-            {
-                mediaSource.SupportsTranscoding = false;
-            }
-
-            if (item is Audio)
-            {
-                _logger.LogInformation(
-                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
-                    user.Username,
-                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
-            }
-            else
-            {
-                _logger.LogInformation(
-                    "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
-                    user.Username,
-                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
-                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
-                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
-            }
-
-            // Beginning of Playback Determination: Attempt DirectPlay first
-            if (mediaSource.SupportsDirectPlay)
-            {
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    mediaSource.SupportsDirectPlay = false;
-                }
-                else
-                {
-                    var supportsDirectStream = mediaSource.SupportsDirectStream;
-
-                    // Dummy this up to fool StreamBuilder
-                    mediaSource.SupportsDirectStream = true;
-                    options.MaxBitrate = maxBitrate;
-
-                    if (item is Audio)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
-                        {
-                            options.ForceDirectPlay = true;
-                        }
-                    }
-                    else if (item is Video)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
-                        {
-                            options.ForceDirectPlay = true;
-                        }
-                    }
-
-                    // The MediaSource supports direct stream, now test to see if the client supports it
-                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                        ? streamBuilder.BuildAudioItem(options)
-                        : streamBuilder.BuildVideoItem(options);
-
-                    if (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectPlay = false;
-                    }
-
-                    // Set this back to what it was
-                    mediaSource.SupportsDirectStream = supportsDirectStream;
-
-                    if (streamInfo != null)
-                    {
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            if (mediaSource.SupportsDirectStream)
-            {
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    mediaSource.SupportsDirectStream = false;
-                }
-                else
-                {
-                    options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
-                    if (item is Audio)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
-                        {
-                            options.ForceDirectStream = true;
-                        }
-                    }
-                    else if (item is Video)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
-                        {
-                            options.ForceDirectStream = true;
-                        }
-                    }
-
-                    // The MediaSource supports direct stream, now test to see if the client supports it
-                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                        ? streamBuilder.BuildAudioItem(options)
-                        : streamBuilder.BuildVideoItem(options);
-
-                    if (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectStream = false;
-                    }
-
-                    if (streamInfo != null)
-                    {
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            if (mediaSource.SupportsTranscoding)
-            {
-                options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
-                // The MediaSource supports direct stream, now test to see if the client supports it
-                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                    ? streamBuilder.BuildAudioItem(options)
-                    : streamBuilder.BuildVideoItem(options);
-
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    if (streamInfo != null)
-                    {
-                        streamInfo.PlaySessionId = playSessionId;
-                        streamInfo.StartPositionTicks = startTimeTicks;
-                        mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
-                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
-                        // Do this after the above so that StartPositionTicks is set
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-                else
-                {
-                    if (streamInfo != null)
-                    {
-                        streamInfo.PlaySessionId = playSessionId;
-
-                        if (streamInfo.PlayMethod == PlayMethod.Transcode)
-                        {
-                            streamInfo.StartPositionTicks = startTimeTicks;
-                            mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-
-                            if (!allowVideoStreamCopy)
-                            {
-                                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
-                            }
-
-                            if (!allowAudioStreamCopy)
-                            {
-                                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                            }
-
-                            mediaSource.TranscodingContainer = streamInfo.Container;
-                            mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-                        }
-
-                        if (!allowAudioStreamCopy)
-                        {
-                            mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                        }
-
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
-                        // Do this after the above so that StartPositionTicks is set
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            foreach (var attachment in mediaSource.MediaAttachments)
-            {
-                attachment.DeliveryUrl = string.Format(
-                    CultureInfo.InvariantCulture,
-                    "/Videos/{0}/{1}/Attachments/{2}",
-                    item.Id,
-                    mediaSource.Id,
-                    attachment.Index);
-            }
-        }
-
-        private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
-        {
-            var authInfo = _authContext.GetAuthorizationInfo(Request);
-
-            var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
-
-            var profile = request.DeviceProfile;
-            if (profile == null)
-            {
-                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
-                if (caps != null)
-                {
-                    profile = caps.DeviceProfile;
-                }
-            }
-
-            if (profile != null)
-            {
-                var item = _libraryManager.GetItemById(request.ItemId);
-
-                SetDeviceSpecificData(
-                    item,
-                    result.MediaSource,
-                    profile,
-                    authInfo,
-                    request.MaxStreamingBitrate,
-                    request.StartTimeTicks ?? 0,
-                    result.MediaSource.Id,
-                    request.AudioStreamIndex,
-                    request.SubtitleStreamIndex,
-                    request.MaxAudioChannels,
-                    request.PlaySessionId,
-                    request.UserId,
-                    request.EnableDirectPlay,
-                    request.EnableDirectStream,
-                    true,
-                    true,
-                    true);
-            }
-            else
-            {
-                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
-                {
-                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
-                }
-            }
-
-            // here was a check if (result.MediaSource != null) but Rider said it will never be null
-            NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
-
-            return result;
-        }
-
-        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
-        {
-            var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
-            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
-
-            mediaSource.TranscodeReasons = info.TranscodeReasons;
-
-            foreach (var profile in profiles)
-            {
-                foreach (var stream in mediaSource.MediaStreams)
-                {
-                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
-                    {
-                        stream.DeliveryMethod = profile.DeliveryMethod;
-
-                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
-                        {
-                            stream.DeliveryUrl = profile.Url.TrimStart('-');
-                            stream.IsExternalUrl = profile.IsExternalUrl;
-                        }
-                    }
-                }
-            }
-        }
-
-        private long? GetMaxBitrate(long? clientMaxBitrate, User user)
-        {
-            var maxBitrate = clientMaxBitrate;
-            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
-
-            if (remoteClientMaxBitrate <= 0)
-            {
-                remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
-            }
-
-            if (remoteClientMaxBitrate > 0)
-            {
-                var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
-
-                _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
-                if (!isInLocalNetwork)
-                {
-                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
-                }
-            }
-
-            return maxBitrate;
-        }
-
-        private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
-        {
-            var originalList = result.MediaSources.ToList();
-
-            result.MediaSources = result.MediaSources.OrderBy(i =>
-                {
-                    // Nothing beats direct playing a file
-                    if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
-                    {
-                        return 0;
-                    }
-
-                    return 1;
-                })
-                .ThenBy(i =>
-                {
-                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
-                    if (i.SupportsDirectPlay || i.SupportsDirectStream)
-                    {
-                        return 0;
-                    }
-
-                    return 1;
-                })
-                .ThenBy(i =>
-                {
-                    return i.Protocol switch
-                    {
-                        MediaProtocol.File => 0,
-                        _ => 1,
-                    };
-                })
-                .ThenBy(i =>
-                {
-                    if (maxBitrate.HasValue && i.Bitrate.HasValue)
-                    {
-                        return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
-                    }
-
-                    return 1;
-                })
-                .ThenBy(originalList.IndexOf)
-                .ToArray();
-        }
     }
 }

+ 0 - 1
Jellyfin.Api/Controllers/NotificationsController.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Constants;

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

@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Item marked as unplayed.</response>
         /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpDelete("Users/{userId}/PlayedItem/{itemId}")]
+        [HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {

+ 10 - 18
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -3,12 +3,12 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers
     {
         private readonly IProviderManager _providerManager;
         private readonly IServerApplicationPaths _applicationPaths;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
 
         /// <summary>
@@ -38,17 +38,17 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
         /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
-        /// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         public RemoteImageController(
             IProviderManager providerManager,
             IServerApplicationPaths applicationPaths,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager)
         {
             _providerManager = providerManager;
             _applicationPaths = applicationPaths;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
         }
 
@@ -244,22 +244,14 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Task.</returns>
         private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
         {
-            using var result = await _httpClient.GetResponse(new HttpRequestOptions
-            {
-                Url = url,
-                BufferContent = false
-            }).ConfigureAwait(false);
-            var ext = result.ContentType.Split('/').Last();
-
+            var httpClient = _httpClientFactory.CreateClient();
+            using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
+            var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
             Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            await using (var stream = result.Content)
-            {
-                await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-                await stream.CopyToAsync(fileStream).ConfigureAwait(false);
-            }
-
+            await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
             Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
                 .ConfigureAwait(false);

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

@@ -413,7 +413,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostFullCapabilities(
-            [FromQuery, Required] string? id,
+            [FromQuery] string? id,
             [FromBody, Required] ClientCapabilities capabilities)
         {
             if (string.IsNullOrWhiteSpace(id))
@@ -480,7 +480,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Password reset providers retrieved.</response>
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
-        [HttpGet("Auto/PasswordResetProviders")]
+        [HttpGet("Auth/PasswordResetProviders")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.RequiresElevation)]
         public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()

+ 141 - 140
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -2,17 +2,20 @@
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
-using System.Net.Http;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
-using Jellyfin.Api.Models.VideoDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -23,27 +26,39 @@ namespace Jellyfin.Api.Controllers
     public class UniversalAudioController : BaseJellyfinApiController
     {
         private readonly IAuthorizationContext _authorizationContext;
-        private readonly MediaInfoController _mediaInfoController;
-        private readonly DynamicHlsController _dynamicHlsController;
-        private readonly AudioController _audioController;
+        private readonly IDeviceManager _deviceManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILogger<UniversalAudioController> _logger;
+        private readonly MediaInfoHelper _mediaInfoHelper;
+        private readonly AudioHelper _audioHelper;
+        private readonly DynamicHlsHelper _dynamicHlsHelper;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
         /// </summary>
         /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
-        /// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
-        /// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
-        /// <param name="audioController">Instance of the <see cref="AudioController"/>.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
+        /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
+        /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+        /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
         public UniversalAudioController(
             IAuthorizationContext authorizationContext,
-            MediaInfoController mediaInfoController,
-            DynamicHlsController dynamicHlsController,
-            AudioController audioController)
+            IDeviceManager deviceManager,
+            ILibraryManager libraryManager,
+            ILogger<UniversalAudioController> logger,
+            MediaInfoHelper mediaInfoHelper,
+            AudioHelper audioHelper,
+            DynamicHlsHelper dynamicHlsHelper)
         {
             _authorizationContext = authorizationContext;
-            _mediaInfoController = mediaInfoController;
-            _dynamicHlsController = dynamicHlsController;
-            _audioController = audioController;
+            _deviceManager = deviceManager;
+            _libraryManager = libraryManager;
+            _logger = logger;
+            _mediaInfoHelper = mediaInfoHelper;
+            _audioHelper = audioHelper;
+            _dynamicHlsHelper = dynamicHlsHelper;
         }
 
         /// <summary>
@@ -95,24 +110,68 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool breakOnNonKeyFrames,
             [FromQuery] bool enableRedirection = true)
         {
-            bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
             _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
 
-            var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
-                itemId,
-                userId,
-                maxStreamingBitrate,
-                startTimeTicks,
-                null,
-                null,
-                maxAudioChannels,
-                mediaSourceId,
-                null,
-                new DeviceProfileDto { DeviceProfile = deviceProfile })
+            var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
+
+            _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
+
+            if (deviceProfile == null)
+            {
+                var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (clientCapabilities != null)
+                {
+                    deviceProfile = clientCapabilities.DeviceProfile;
+                }
+            }
+
+            var info = await _mediaInfoHelper.GetPlaybackInfo(
+                    itemId,
+                    userId,
+                    mediaSourceId)
                 .ConfigureAwait(false);
-            var mediaSource = playbackInfoResult.Value.MediaSources[0];
 
+            if (deviceProfile != null)
+            {
+                // set device specific data
+                var item = _libraryManager.GetItemById(itemId);
+
+                foreach (var sourceInfo in info.MediaSources)
+                {
+                    _mediaInfoHelper.SetDeviceSpecificData(
+                        item,
+                        sourceInfo,
+                        deviceProfile,
+                        authInfo,
+                        maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+                        startTimeTicks ?? 0,
+                        mediaSourceId ?? string.Empty,
+                        null,
+                        null,
+                        maxAudioChannels,
+                        info!.PlaySessionId!,
+                        userId ?? Guid.Empty,
+                        true,
+                        true,
+                        true,
+                        true,
+                        true,
+                        Request.HttpContext.Connection.RemoteIpAddress.ToString());
+                }
+
+                _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+            }
+
+            if (info.MediaSources != null)
+            {
+                foreach (var source in info.MediaSources)
+                {
+                    _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
+                }
+            }
+
+            var mediaSource = info.MediaSources![0];
             if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
             {
                 if (enableRedirection)
@@ -127,129 +186,71 @@ namespace Jellyfin.Api.Controllers
             var isStatic = mediaSource.SupportsDirectStream;
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
-                var transcodingProfile = deviceProfile.TranscodingProfiles[0];
-
                 // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
                 // TODO: remove this when we switch back to the segment muxer
                 var supportedHlsContainers = new[] { "mpegts", "fmp4" };
 
-                if (isHeadRequest)
+                var dynamicHlsRequestDto = new HlsAudioRequestDto
                 {
-                    _dynamicHlsController.Request.Method = HttpMethod.Head.Method;
-                }
-
-                return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
-                    itemId,
-                    ".m3u8",
-                    isStatic,
-                    null,
-                    null,
-                    null,
-                    playbackInfoResult.Value.PlaySessionId,
+                    Id = itemId,
+                    Container = ".m3u8",
+                    Static = isStatic,
+                    PlaySessionId = info.PlaySessionId,
                     // fallback to mpegts if device reports some weird value unsupported by hls
-                    Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
-                    null,
-                    null,
-                    mediaSource.Id,
-                    deviceId,
-                    transcodingProfile.AudioCodec,
-                    null,
-                    null,
-                    null,
-                    transcodingProfile.BreakOnNonKeyFrames,
-                    maxAudioSampleRate,
-                    maxAudioBitDepth,
-                    null,
-                    isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                    maxAudioChannels,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    startTimeTicks,
-                    null,
-                    null,
-                    null,
-                    null,
-                    SubtitleDeliveryMethod.Hls,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                    null,
-                    null,
-                    EncodingContext.Static,
-                    new Dictionary<string, string>())
+                    SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+                    MediaSourceId = mediaSourceId,
+                    DeviceId = deviceId,
+                    AudioCodec = audioCodec,
+                    EnableAutoStreamCopy = true,
+                    AllowAudioStreamCopy = true,
+                    AllowVideoStreamCopy = true,
+                    BreakOnNonKeyFrames = breakOnNonKeyFrames,
+                    AudioSampleRate = maxAudioSampleRate,
+                    MaxAudioChannels = maxAudioChannels,
+                    MaxAudioBitDepth = maxAudioBitDepth,
+                    AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                    StartTimeTicks = startTimeTicks,
+                    SubtitleMethod = SubtitleDeliveryMethod.Hls,
+                    RequireAvc = true,
+                    DeInterlace = true,
+                    RequireNonAnamorphic = true,
+                    EnableMpegtsM2TsMode = true,
+                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                    Context = EncodingContext.Static,
+                    StreamOptions = new Dictionary<string, string>(),
+                    EnableAdaptiveBitrateStreaming = true
+                };
+
+                return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
                     .ConfigureAwait(false);
             }
-            else
+
+            var audioStreamingDto = new StreamingRequestDto
             {
-                if (isHeadRequest)
-                {
-                    _audioController.Request.Method = HttpMethod.Head.Method;
-                }
+                Id = itemId,
+                Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
+                Static = isStatic,
+                PlaySessionId = info.PlaySessionId,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = true,
+                AllowAudioStreamCopy = true,
+                AllowVideoStreamCopy = true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames,
+                AudioSampleRate = maxAudioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = maxAudioChannels,
+                CopyTimestamps = true,
+                StartTimeTicks = startTimeTicks,
+                SubtitleMethod = SubtitleDeliveryMethod.Embed,
+                TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                Context = EncodingContext.Static
+            };
 
-                return await _audioController.GetAudioStream(
-                    itemId,
-                    isStatic ? null : ("." + mediaSource.TranscodingContainer),
-                    isStatic,
-                    null,
-                    null,
-                    null,
-                    playbackInfoResult.Value.PlaySessionId,
-                    null,
-                    null,
-                    null,
-                    mediaSource.Id,
-                    deviceId,
-                    audioCodec,
-                    null,
-                    null,
-                    null,
-                    breakOnNonKeyFrames,
-                    maxAudioSampleRate,
-                    maxAudioBitDepth,
-                    isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                    null,
-                    maxAudioChannels,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    startTimeTicks,
-                    null,
-                    null,
-                    null,
-                    null,
-                    SubtitleDeliveryMethod.Embed,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                    null,
-                    null,
-                    null,
-                    null)
-                    .ConfigureAwait(false);
-            }
+            return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
         }
 
         private DeviceProfile GetDeviceProfile(

+ 4 - 5
Jellyfin.Api/Controllers/VideosController.cs

@@ -470,8 +470,8 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                using var httpClient = _httpClientFactory.CreateClient();
-                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
+                var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
             }
 
             if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
@@ -507,7 +507,7 @@ namespace Jellyfin.Api.Controllers
                     state.MediaPath,
                     contentType,
                     isHeadRequest,
-                    this);
+                    HttpContext);
             }
 
             // Need to start ffmpeg (because media can't be returned directly)
@@ -517,10 +517,9 @@ namespace Jellyfin.Api.Controllers
             return await FileStreamResponseHelpers.GetTranscodedFile(
                 state,
                 isHeadRequest,
-                this,
+                HttpContext,
                 _transcodingJobHelper,
                 ffmpegCommandLineArguments,
-                Request,
                 _transcodingJobType,
                 cancellationTokenSource).ConfigureAwait(false);
         }

+ 195 - 0
Jellyfin.Api/Helpers/AudioHelper.cs

@@ -0,0 +1,195 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Audio helper.
+    /// </summary>
+    public class AudioHelper
+    {
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IHttpContextAccessor _httpContextAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioHelper"/> class.
+        /// </summary>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public AudioHelper(
+            IDlnaManager dlnaManager,
+            IAuthorizationContext authContext,
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            IHttpClientFactory httpClientFactory,
+            IHttpContextAccessor httpContextAccessor)
+        {
+            _dlnaManager = dlnaManager;
+            _authContext = authContext;
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _httpClientFactory = httpClientFactory;
+            _httpContextAccessor = httpContextAccessor;
+        }
+
+        /// <summary>
+        /// Get audio stream.
+        /// </summary>
+        /// <param name="transcodingJobType">Transcoding job type.</param>
+        /// <param name="streamingRequest">Streaming controller.Request dto.</param>
+        /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+        public async Task<ActionResult> GetAudioStream(
+            TranscodingJobType transcodingJobType,
+            StreamingRequestDto streamingRequest)
+        {
+            bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
+            var cancellationTokenSource = new CancellationTokenSource();
+
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    _httpContextAccessor.HttpContext.Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            if (streamingRequest.Static && state.DirectStreamProvider != null)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+
+                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
+                    .ConfigureAwait(false);
+
+                // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+                return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!);
+            }
+
+            // Static remote stream
+            if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+
+                var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
+            }
+
+            if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File)
+            {
+                return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically");
+            }
+
+            var outputPath = state.OutputFilePath;
+            var outputPathExists = System.IO.File.Exists(outputPath);
+
+            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+            StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
+
+            // Static stream
+            if (streamingRequest.Static)
+            {
+                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+                if (state.MediaSource.IsInfiniteStream)
+                {
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
+
+                    return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
+                }
+
+                return FileStreamResponseHelpers.GetStaticFileResult(
+                    state.MediaPath,
+                    contentType,
+                    isHeadRequest,
+                    _httpContextAccessor.HttpContext);
+            }
+
+            // Need to start ffmpeg (because media can't be returned directly)
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+            var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+            var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+            return await FileStreamResponseHelpers.GetTranscodedFile(
+                state,
+                isHeadRequest,
+                _httpContextAccessor.HttpContext,
+                _transcodingJobHelper,
+                ffmpegCommandLineArguments,
+                transcodingJobType,
+                cancellationTokenSource).ConfigureAwait(false);
+        }
+    }
+}

+ 550 - 0
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -0,0 +1,550 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Security.Claims;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Dynamic hls helper.
+    /// </summary>
+    public class DynamicHlsHelper
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly INetworkManager _networkManager;
+        private readonly ILogger<DynamicHlsHelper> _logger;
+        private readonly IHttpContextAccessor _httpContextAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public DynamicHlsHelper(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDlnaManager dlnaManager,
+            IAuthorizationContext authContext,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            INetworkManager networkManager,
+            ILogger<DynamicHlsHelper> logger,
+            IHttpContextAccessor httpContextAccessor)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dlnaManager = dlnaManager;
+            _authContext = authContext;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _networkManager = networkManager;
+            _logger = logger;
+            _httpContextAccessor = httpContextAccessor;
+        }
+
+        /// <summary>
+        /// Get master hls playlist.
+        /// </summary>
+        /// <param name="transcodingJobType">Transcoding job type.</param>
+        /// <param name="streamingRequest">Streaming request dto.</param>
+        /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+        /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
+        public async Task<ActionResult> GetMasterHlsPlaylist(
+            TranscodingJobType transcodingJobType,
+            StreamingRequestDto streamingRequest,
+            bool enableAdaptiveBitrateStreaming)
+        {
+            var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head;
+            var cancellationTokenSource = new CancellationTokenSource();
+            return await GetMasterPlaylistInternal(
+                streamingRequest,
+                isHeadRequest,
+                enableAdaptiveBitrateStreaming,
+                transcodingJobType,
+                cancellationTokenSource).ConfigureAwait(false);
+        }
+
+        private async Task<ActionResult> GetMasterPlaylistInternal(
+            StreamingRequestDto streamingRequest,
+            bool isHeadRequest,
+            bool enableAdaptiveBitrateStreaming,
+            TranscodingJobType transcodingJobType,
+            CancellationTokenSource cancellationTokenSource)
+        {
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    _httpContextAccessor.HttpContext.Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
+            if (isHeadRequest)
+            {
+                return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
+            }
+
+            var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+
+            var isLiveStream = state.IsSegmentedLiveStream;
+
+            var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
+
+            // from universal audio service
+            if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
+            {
+                queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
+            }
+
+            // from universal audio service
+            if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
+            {
+                queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
+            }
+
+            // Main stream
+            var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+
+            playlistUrl += queryString;
+
+            var subtitleStreams = state.MediaSource
+                .MediaStreams
+                .Where(i => i.IsTextSubtitleStream)
+                .ToList();
+
+            var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
+                ? "subs"
+                : null;
+
+            // If we're burning in subtitles then don't add additional subs to the manifest
+            if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+            {
+                subtitleGroup = null;
+            }
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User);
+            }
+
+            AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+
+            if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress))
+            {
+                var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
+
+                // By default, vary by just 200k
+                var variation = GetBitrateVariation(totalBitrate);
+
+                var newBitrate = totalBitrate - variation;
+                var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+
+                variation *= 2;
+                newBitrate = totalBitrate - variation;
+                variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+            }
+
+            return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+        }
+
+        private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+        {
+            builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+                .Append(bitrate.ToString(CultureInfo.InvariantCulture))
+                .Append(",AVERAGE-BANDWIDTH=")
+                .Append(bitrate.ToString(CultureInfo.InvariantCulture));
+
+            AppendPlaylistCodecsField(builder, state);
+
+            AppendPlaylistResolutionField(builder, state);
+
+            AppendPlaylistFramerateField(builder, state);
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                builder.Append(",SUBTITLES=\"")
+                    .Append(subtitleGroup)
+                    .Append('"');
+            }
+
+            builder.Append(Environment.NewLine);
+            builder.AppendLine(url);
+        }
+
+        /// <summary>
+        /// Appends a CODECS field containing formatted strings of
+        /// the active streams output video and audio codecs.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
+        {
+            // Video
+            string videoCodecs = string.Empty;
+            int? videoCodecLevel = GetOutputVideoCodecLevel(state);
+            if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
+            {
+                videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+            }
+
+            // Audio
+            string audioCodecs = string.Empty;
+            if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+            {
+                audioCodecs = GetPlaylistAudioCodecs(state);
+            }
+
+            StringBuilder codecs = new StringBuilder();
+
+            codecs.Append(videoCodecs);
+
+            if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
+            {
+                codecs.Append(',');
+            }
+
+            codecs.Append(audioCodecs);
+
+            if (codecs.Length > 1)
+            {
+                builder.Append(",CODECS=\"")
+                    .Append(codecs)
+                    .Append('"');
+            }
+        }
+
+        /// <summary>
+        /// Appends a RESOLUTION field containing the resolution of the output stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+        {
+            if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
+            {
+                builder.Append(",RESOLUTION=")
+                    .Append(state.OutputWidth.GetValueOrDefault())
+                    .Append('x')
+                    .Append(state.OutputHeight.GetValueOrDefault());
+            }
+        }
+
+        /// <summary>
+        /// Appends a FRAME-RATE field containing the framerate of the output stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+        {
+            double? framerate = null;
+            if (state.TargetFramerate.HasValue)
+            {
+                framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
+            }
+            else if (state.VideoStream?.RealFrameRate != null)
+            {
+                framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
+            }
+
+            if (framerate.HasValue)
+            {
+                builder.Append(",FRAME-RATE=")
+                    .Append(framerate.Value);
+            }
+        }
+
+        private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+        {
+            // Within the local network this will likely do more harm than good.
+            var ip = RequestHelpers.NormalizeIp(ipAddress).ToString();
+            if (_networkManager.IsInLocalNetwork(ip))
+            {
+                return false;
+            }
+
+            if (!enableAdaptiveBitrateStreaming)
+            {
+                return false;
+            }
+
+            if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
+            {
+                // Opening live streams is so slow it's not even worth it
+                return false;
+            }
+
+            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            {
+                return false;
+            }
+
+            if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
+            {
+                return false;
+            }
+
+            if (!state.IsOutputVideo)
+            {
+                return false;
+            }
+
+            // Having problems in android
+            return false;
+            // return state.VideoRequest.VideoBitRate.HasValue;
+        }
+
+        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
+        {
+            var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
+            const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
+
+            foreach (var stream in subtitles)
+            {
+                var name = stream.DisplayTitle;
+
+                var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
+                var isForced = stream.IsForced;
+
+                var url = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
+                    state.Request.MediaSourceId,
+                    stream.Index.ToString(CultureInfo.InvariantCulture),
+                    30.ToString(CultureInfo.InvariantCulture),
+                    ClaimHelpers.GetToken(user));
+
+                var line = string.Format(
+                    CultureInfo.InvariantCulture,
+                    Format,
+                    name,
+                    isDefault ? "YES" : "NO",
+                    isForced ? "YES" : "NO",
+                    url,
+                    stream.Language ?? "Unknown");
+
+                builder.AppendLine(line);
+            }
+        }
+
+        /// <summary>
+        /// Get the H.26X level of the output video stream.
+        /// </summary>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>H.26X level of the output video stream.</returns>
+        private int? GetOutputVideoCodecLevel(StreamState state)
+        {
+            string? levelString;
+            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+                && state.VideoStream.Level.HasValue)
+            {
+                levelString = state.VideoStream?.Level.ToString();
+            }
+            else
+            {
+                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+            }
+
+            if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
+            {
+                return parsedLevel;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>Formatted audio codec string.</returns>
+        private string GetPlaylistAudioCodecs(StreamState state)
+        {
+            if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+            {
+                string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
+                return HlsCodecStringHelpers.GetAACString(profile);
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetMP3String();
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetAC3String();
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetEAC3String();
+            }
+
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output video codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <param name="codec">Video codec.</param>
+        /// <param name="level">Video level.</param>
+        /// <returns>Formatted video codec string.</returns>
+        private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+        {
+            if (level == 0)
+            {
+                // This is 0 when there's no requested H.26X level in the device profile
+                // and the source is not encoded in H.26X
+                _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
+                return string.Empty;
+            }
+
+            if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+                return HlsCodecStringHelpers.GetH264String(profile, level);
+            }
+
+            if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
+
+                return HlsCodecStringHelpers.GetH265String(profile, level);
+            }
+
+            return string.Empty;
+        }
+
+        private int GetBitrateVariation(int bitrate)
+        {
+            // By default, vary by just 50k
+            var variation = 50000;
+
+            if (bitrate >= 10000000)
+            {
+                variation = 2000000;
+            }
+            else if (bitrate >= 5000000)
+            {
+                variation = 1500000;
+            }
+            else if (bitrate >= 3000000)
+            {
+                variation = 1000000;
+            }
+            else if (bitrate >= 2000000)
+            {
+                variation = 500000;
+            }
+            else if (bitrate >= 1000000)
+            {
+                variation = 300000;
+            }
+            else if (bitrate >= 600000)
+            {
+                variation = 200000;
+            }
+            else if (bitrate >= 400000)
+            {
+                variation = 100000;
+            }
+
+            return variation;
+        }
+
+        private string ReplaceBitrate(string url, int oldValue, int newValue)
+        {
+            return url.Replace(
+                "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
+                "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
+                StringComparison.OrdinalIgnoreCase);
+        }
+    }
+}

+ 19 - 20
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -22,31 +22,32 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
-        /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
         /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
+        /// <param name="httpContext">The current http context.</param>
         /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
         public static async Task<ActionResult> GetStaticRemoteStreamResult(
             StreamState state,
             bool isHeadRequest,
-            ControllerBase controller,
-            HttpClient httpClient)
+            HttpClient httpClient,
+            HttpContext httpContext)
         {
             if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
             {
                 httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
             }
 
-            using var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
+            // Can't dispose the response as it's required up the call chain.
+            var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
             var contentType = response.Content.Headers.ContentType.ToString();
 
-            controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
+            httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
 
             if (isHeadRequest)
             {
-                return controller.File(Array.Empty<byte>(), contentType);
+                return new FileContentResult(Array.Empty<byte>(), contentType);
             }
 
-            return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
+            return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
         }
 
         /// <summary>
@@ -55,23 +56,23 @@ namespace Jellyfin.Api.Helpers
         /// <param name="path">The path to the file.</param>
         /// <param name="contentType">The content type of the file.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
-        /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
+        /// <param name="httpContext">The current http context.</param>
         /// <returns>An <see cref="ActionResult"/> the file.</returns>
         public static ActionResult GetStaticFileResult(
             string path,
             string contentType,
             bool isHeadRequest,
-            ControllerBase controller)
+            HttpContext httpContext)
         {
-            controller.Response.ContentType = contentType;
+            httpContext.Response.ContentType = contentType;
 
             // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
             if (isHeadRequest)
             {
-                return controller.NoContent();
+                return new NoContentResult();
             }
 
-            return controller.PhysicalFile(path, contentType);
+            return new PhysicalFileResult(path, contentType);
         }
 
         /// <summary>
@@ -79,34 +80,32 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
-        /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
+        /// <param name="httpContext">The current http context.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
         /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
-        /// <param name="request">The <see cref="HttpRequest"/> starting the transcoding.</param>
         /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
         /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
         /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
         public static async Task<ActionResult> GetTranscodedFile(
             StreamState state,
             bool isHeadRequest,
-            ControllerBase controller,
+            HttpContext httpContext,
             TranscodingJobHelper transcodingJobHelper,
             string ffmpegCommandLineArguments,
-            HttpRequest request,
             TranscodingJobType transcodingJobType,
             CancellationTokenSource cancellationTokenSource)
         {
             // Use the command line args with a dummy playlist path
             var outputPath = state.OutputFilePath;
 
-            controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
+            httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
 
             var contentType = state.GetMimeType(outputPath);
 
             // Headers only
             if (isHeadRequest)
             {
-                return controller.File(Array.Empty<byte>(), contentType);
+                return new FileContentResult(Array.Empty<byte>(), contentType);
             }
 
             var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
@@ -116,7 +115,7 @@ namespace Jellyfin.Api.Helpers
                 TranscodingJobDto? job;
                 if (!File.Exists(outputPath))
                 {
-                    job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
+                    job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
                 }
                 else
                 {
@@ -127,7 +126,7 @@ namespace Jellyfin.Api.Helpers
                 var memoryStream = new MemoryStream();
                 await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
                 memoryStream.Position = 0;
-                return controller.File(memoryStream, contentType);
+                return new FileStreamResult(memoryStream, contentType);
             }
             finally
             {

+ 573 - 0
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -0,0 +1,573 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Media info helper.
+    /// </summary>
+    public class MediaInfoHelper
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly ILogger<MediaInfoHelper> _logger;
+        private readonly INetworkManager _networkManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly IAuthorizationContext _authContext;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        public MediaInfoHelper(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IMediaSourceManager mediaSourceManager,
+            IMediaEncoder mediaEncoder,
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger<MediaInfoHelper> logger,
+            INetworkManager networkManager,
+            IDeviceManager deviceManager,
+            IAuthorizationContext authContext)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+            _mediaEncoder = mediaEncoder;
+            _serverConfigurationManager = serverConfigurationManager;
+            _logger = logger;
+            _networkManager = networkManager;
+            _deviceManager = deviceManager;
+            _authContext = authContext;
+        }
+
+        /// <summary>
+        /// Get playback info.
+        /// </summary>
+        /// <param name="id">Item id.</param>
+        /// <param name="userId">User Id.</param>
+        /// <param name="mediaSourceId">Media source id.</param>
+        /// <param name="liveStreamId">Live stream id.</param>
+        /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
+        public async Task<PlaybackInfoResponse> GetPlaybackInfo(
+            Guid id,
+            Guid? userId,
+            string? mediaSourceId = null,
+            string? liveStreamId = null)
+        {
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
+            var item = _libraryManager.GetItemById(id);
+            var result = new PlaybackInfoResponse();
+
+            MediaSourceInfo[] mediaSources;
+            if (string.IsNullOrWhiteSpace(liveStreamId))
+            {
+                // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
+                var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
+
+                if (string.IsNullOrWhiteSpace(mediaSourceId))
+                {
+                    mediaSources = mediaSourcesList.ToArray();
+                }
+                else
+                {
+                    mediaSources = mediaSourcesList
+                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+                        .ToArray();
+                }
+            }
+            else
+            {
+                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+
+                mediaSources = new[] { mediaSource };
+            }
+
+            if (mediaSources.Length == 0)
+            {
+                result.MediaSources = Array.Empty<MediaSourceInfo>();
+
+                result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+            }
+            else
+            {
+                // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+                // Should we move this directly into MediaSourceManager?
+                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+
+                result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// SetDeviceSpecificData.
+        /// </summary>
+        /// <param name="item">Item to set data for.</param>
+        /// <param name="mediaSource">Media source info.</param>
+        /// <param name="profile">Device profile.</param>
+        /// <param name="auth">Authorization info.</param>
+        /// <param name="maxBitrate">Max bitrate.</param>
+        /// <param name="startTimeTicks">Start time ticks.</param>
+        /// <param name="mediaSourceId">Media source id.</param>
+        /// <param name="audioStreamIndex">Audio stream index.</param>
+        /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
+        /// <param name="maxAudioChannels">Max audio channels.</param>
+        /// <param name="playSessionId">Play session id.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="enableDirectPlay">Enable direct play.</param>
+        /// <param name="enableDirectStream">Enable direct stream.</param>
+        /// <param name="enableTranscoding">Enable transcoding.</param>
+        /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
+        /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
+        /// <param name="ipAddress">Requesting IP address.</param>
+        public void SetDeviceSpecificData(
+            BaseItem item,
+            MediaSourceInfo mediaSource,
+            DeviceProfile profile,
+            AuthorizationInfo auth,
+            long? maxBitrate,
+            long startTimeTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex,
+            int? maxAudioChannels,
+            string playSessionId,
+            Guid userId,
+            bool enableDirectPlay,
+            bool enableDirectStream,
+            bool enableTranscoding,
+            bool allowVideoStreamCopy,
+            bool allowAudioStreamCopy,
+            string ipAddress)
+        {
+            var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
+
+            var options = new VideoOptions
+            {
+                MediaSources = new[] { mediaSource },
+                Context = EncodingContext.Streaming,
+                DeviceId = auth.DeviceId,
+                ItemId = item.Id,
+                Profile = profile,
+                MaxAudioChannels = maxAudioChannels
+            };
+
+            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+            {
+                options.MediaSourceId = mediaSourceId;
+                options.AudioStreamIndex = audioStreamIndex;
+                options.SubtitleStreamIndex = subtitleStreamIndex;
+            }
+
+            var user = _userManager.GetUserById(userId);
+
+            if (!enableDirectPlay)
+            {
+                mediaSource.SupportsDirectPlay = false;
+            }
+
+            if (!enableDirectStream)
+            {
+                mediaSource.SupportsDirectStream = false;
+            }
+
+            if (!enableTranscoding)
+            {
+                mediaSource.SupportsTranscoding = false;
+            }
+
+            if (item is Audio)
+            {
+                _logger.LogInformation(
+                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+            }
+            else
+            {
+                _logger.LogInformation(
+                    "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+            }
+
+            // Beginning of Playback Determination: Attempt DirectPlay first
+            if (mediaSource.SupportsDirectPlay)
+            {
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    mediaSource.SupportsDirectPlay = false;
+                }
+                else
+                {
+                    var supportsDirectStream = mediaSource.SupportsDirectStream;
+
+                    // Dummy this up to fool StreamBuilder
+                    mediaSource.SupportsDirectStream = true;
+                    options.MaxBitrate = maxBitrate;
+
+                    if (item is Audio)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
+                        {
+                            options.ForceDirectPlay = true;
+                        }
+                    }
+                    else if (item is Video)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+                        {
+                            options.ForceDirectPlay = true;
+                        }
+                    }
+
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                        ? streamBuilder.BuildAudioItem(options)
+                        : streamBuilder.BuildVideoItem(options);
+
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectPlay = false;
+                    }
+
+                    // Set this back to what it was
+                    mediaSource.SupportsDirectStream = supportsDirectStream;
+
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            if (mediaSource.SupportsDirectStream)
+            {
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    mediaSource.SupportsDirectStream = false;
+                }
+                else
+                {
+                    options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
+
+                    if (item is Audio)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
+                        {
+                            options.ForceDirectStream = true;
+                        }
+                    }
+                    else if (item is Video)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+                        {
+                            options.ForceDirectStream = true;
+                        }
+                    }
+
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                        ? streamBuilder.BuildAudioItem(options)
+                        : streamBuilder.BuildVideoItem(options);
+
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectStream = false;
+                    }
+
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            if (mediaSource.SupportsTranscoding)
+            {
+                options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
+
+                // The MediaSource supports direct stream, now test to see if the client supports it
+                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                    ? streamBuilder.BuildAudioItem(options)
+                    : streamBuilder.BuildVideoItem(options);
+
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    if (streamInfo != null)
+                    {
+                        streamInfo.PlaySessionId = playSessionId;
+                        streamInfo.StartPositionTicks = startTimeTicks;
+                        mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                        mediaSource.TranscodingContainer = streamInfo.Container;
+                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
+                        // Do this after the above so that StartPositionTicks is set
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+                else
+                {
+                    if (streamInfo != null)
+                    {
+                        streamInfo.PlaySessionId = playSessionId;
+
+                        if (streamInfo.PlayMethod == PlayMethod.Transcode)
+                        {
+                            streamInfo.StartPositionTicks = startTimeTicks;
+                            mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+
+                            if (!allowVideoStreamCopy)
+                            {
+                                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+                            }
+
+                            if (!allowAudioStreamCopy)
+                            {
+                                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                            }
+
+                            mediaSource.TranscodingContainer = streamInfo.Container;
+                            mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+                        }
+
+                        if (!allowAudioStreamCopy)
+                        {
+                            mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                        }
+
+                        mediaSource.TranscodingContainer = streamInfo.Container;
+                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
+                        // Do this after the above so that StartPositionTicks is set
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            foreach (var attachment in mediaSource.MediaAttachments)
+            {
+                attachment.DeliveryUrl = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "/Videos/{0}/{1}/Attachments/{2}",
+                    item.Id,
+                    mediaSource.Id,
+                    attachment.Index);
+            }
+        }
+
+        /// <summary>
+        /// Sort media source.
+        /// </summary>
+        /// <param name="result">Playback info response.</param>
+        /// <param name="maxBitrate">Max bitrate.</param>
+        public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
+        {
+            var originalList = result.MediaSources.ToList();
+
+            result.MediaSources = result.MediaSources.OrderBy(i =>
+                {
+                    // Nothing beats direct playing a file
+                    if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
+                    {
+                        return 0;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(i =>
+                {
+                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+                    if (i.SupportsDirectPlay || i.SupportsDirectStream)
+                    {
+                        return 0;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(i =>
+                {
+                    return i.Protocol switch
+                    {
+                        MediaProtocol.File => 0,
+                        _ => 1,
+                    };
+                })
+                .ThenBy(i =>
+                {
+                    if (maxBitrate.HasValue && i.Bitrate.HasValue)
+                    {
+                        return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(originalList.IndexOf)
+                .ToArray();
+        }
+
+        /// <summary>
+        /// Open media source.
+        /// </summary>
+        /// <param name="httpRequest">Http Request.</param>
+        /// <param name="request">Live stream request.</param>
+        /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
+        public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
+        {
+            var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
+
+            var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
+
+            var profile = request.DeviceProfile;
+            if (profile == null)
+            {
+                var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (clientCapabilities != null)
+                {
+                    profile = clientCapabilities.DeviceProfile;
+                }
+            }
+
+            if (profile != null)
+            {
+                var item = _libraryManager.GetItemById(request.ItemId);
+
+                SetDeviceSpecificData(
+                    item,
+                    result.MediaSource,
+                    profile,
+                    authInfo,
+                    request.MaxStreamingBitrate,
+                    request.StartTimeTicks ?? 0,
+                    result.MediaSource.Id,
+                    request.AudioStreamIndex,
+                    request.SubtitleStreamIndex,
+                    request.MaxAudioChannels,
+                    request.PlaySessionId,
+                    request.UserId,
+                    request.EnableDirectPlay,
+                    request.EnableDirectStream,
+                    true,
+                    true,
+                    true,
+                    httpRequest.HttpContext.Connection.RemoteIpAddress.ToString());
+            }
+            else
+            {
+                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+                {
+                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+                }
+            }
+
+            // here was a check if (result.MediaSource != null) but Rider said it will never be null
+            NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Normalize media source container.
+        /// </summary>
+        /// <param name="mediaSource">Media source.</param>
+        /// <param name="profile">Device profile.</param>
+        /// <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);
+        }
+
+        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+        {
+            var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
+            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+
+            mediaSource.TranscodeReasons = info.TranscodeReasons;
+
+            foreach (var profile in profiles)
+            {
+                foreach (var stream in mediaSource.MediaStreams)
+                {
+                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
+                    {
+                        stream.DeliveryMethod = profile.DeliveryMethod;
+
+                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
+                        {
+                            stream.DeliveryUrl = profile.Url.TrimStart('-');
+                            stream.IsExternalUrl = profile.IsExternalUrl;
+                        }
+                    }
+                }
+            }
+        }
+
+        private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
+        {
+            var maxBitrate = clientMaxBitrate;
+            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+
+            if (remoteClientMaxBitrate <= 0)
+            {
+                remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
+            }
+
+            if (remoteClientMaxBitrate > 0)
+            {
+                var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
+
+                _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
+                if (!isInLocalNetwork)
+                {
+                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
+                }
+            }
+
+            return maxBitrate;
+        }
+    }
+}

+ 44 - 0
Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs

@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Jellyfin.Api.TypeConverters
+{
+    /// <summary>
+    /// Custom datetime parser.
+    /// </summary>
+    public class DateTimeTypeConverter : TypeConverter
+    {
+        /// <inheritdoc />
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            if (sourceType == typeof(string))
+            {
+                return true;
+            }
+
+            return base.CanConvertFrom(context, sourceType);
+        }
+
+        /// <inheritdoc />
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            if (value is string dateString)
+            {
+                // Mark Played Item.
+                if (DateTime.TryParseExact(dateString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
+                {
+                    return dateTime;
+                }
+
+                // Get Activity Logs.
+                if (DateTime.TryParse(dateString, null, DateTimeStyles.RoundtripKind, out dateTime))
+                {
+                    return dateTime;
+                }
+            }
+
+            return base.ConvertFrom(context, culture, value);
+        }
+    }
+}

+ 1 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -154,6 +154,7 @@ namespace Jellyfin.Server.Extensions
                     opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 
                     opts.OutputFormatters.Add(new CssOutputFormatter());
+                    opts.OutputFormatters.Add(new XmlOutputFormatter());
                 })
 
                 // Clear app parts to avoid other assemblies being picked up

+ 31 - 0
Jellyfin.Server/Formatters/XmlOutputFormatter.cs

@@ -0,0 +1,31 @@
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Formatters;
+
+namespace Jellyfin.Server.Formatters
+{
+    /// <summary>
+    /// Xml output formatter.
+    /// </summary>
+    public class XmlOutputFormatter : TextOutputFormatter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
+        /// </summary>
+        public XmlOutputFormatter()
+        {
+            SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
+            SupportedMediaTypes.Add("text/xml;charset=UTF-8");
+            SupportedEncodings.Add(Encoding.UTF8);
+            SupportedEncodings.Add(Encoding.Unicode);
+        }
+
+        /// <inheritdoc />
+        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
+        {
+            return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+        }
+    }
+}

+ 6 - 1
Jellyfin.Server/Startup.cs

@@ -1,4 +1,6 @@
-using System.Net.Http;
+using System;
+using System.ComponentModel;
+using Jellyfin.Api.TypeConverters;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Models;
@@ -94,6 +96,9 @@ namespace Jellyfin.Server
             });
 
             app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+
+            // Add type descriptor for legacy datetime parsing.
+            TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
         }
     }
 }

+ 3 - 3
MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs

@@ -8,7 +8,7 @@ using System.Text.Json.Serialization;
 namespace MediaBrowser.Common.Json.Converters
 {
     /// <summary>
-    /// Long to String JSON converter.
+    /// Parse JSON string as long.
     /// Javascript does not support 64-bit integers.
     /// </summary>
     public class JsonInt64Converter : JsonConverter<long>
@@ -43,14 +43,14 @@ namespace MediaBrowser.Common.Json.Converters
         }
 
         /// <summary>
-        /// Write long to JSON string.
+        /// Write long to JSON long.
         /// </summary>
         /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
         /// <param name="value">Value to write.</param>
         /// <param name="options">Options.</param>
         public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
         {
-            writer.WriteStringValue(value.ToString(NumberFormatInfo.InvariantInfo));
+            writer.WriteNumberValue(value);
         }
     }
 }

+ 0 - 24
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -2,7 +2,6 @@ using System.Collections.Generic;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Controller.Dto
 {
@@ -11,20 +10,6 @@ namespace MediaBrowser.Controller.Dto
     /// </summary>
     public interface IDtoService
     {
-        /// <summary>
-        /// Gets the dto id.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        string GetDtoId(BaseItem item);
-
-        /// <summary>
-        /// Attaches the primary image aspect ratio.
-        /// </summary>
-        /// <param name="dto">The dto.</param>
-        /// <param name="item">The item.</param>
-        void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item);
-
         /// <summary>
         /// Gets the primary image aspect ratio.
         /// </summary>
@@ -32,15 +17,6 @@ namespace MediaBrowser.Controller.Dto
         /// <returns>System.Nullable&lt;System.Double&gt;.</returns>
         double? GetPrimaryImageAspectRatio(BaseItem item);
 
-        /// <summary>
-        /// Gets the base item dto.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="fields">The fields.</param>
-        /// <param name="user">The user.</param>
-        /// <param name="owner">The owner.</param>
-        BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null);
-
         /// <summary>
         /// Gets the base item dto.
         /// </summary>

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

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
@@ -157,7 +158,7 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="url">The URL.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{HttpResponseInfo}.</returns>
-        Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
+        Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
 
         Dictionary<Guid, Guid> GetRefreshQueue();
 

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

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Net;
@@ -34,6 +35,6 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="url">The URL.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{HttpResponseInfo}.</returns>
-        Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+        Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken);
     }
 }

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

@@ -1,3 +1,4 @@
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Net;
@@ -12,6 +13,6 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="url">The URL.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{HttpResponseInfo}.</returns>
-        Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+        Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken);
     }
 }

+ 0 - 29
MediaBrowser.Model/Extensions/ListHelper.cs

@@ -1,29 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Extensions
-{
-    // TODO: @bond remove
-    public static class ListHelper
-    {
-        public static bool ContainsIgnoreCase(string[] list, string value)
-        {
-            if (value == null)
-            {
-                throw new ArgumentNullException(nameof(value));
-            }
-
-            foreach (var item in list)
-            {
-                if (string.Equals(item, value, StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-    }
-}

+ 20 - 6
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -465,9 +465,16 @@ namespace MediaBrowser.Providers.Manager
 
                 try
                 {
-                    var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+                    using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+                    await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
 
-                    await _providerManager.SaveImage(item, response.Content, response.ContentType, type, null, cancellationToken).ConfigureAwait(false);
+                    await _providerManager.SaveImage(
+                        item,
+                        stream,
+                        response.Content.Headers.ContentType.MediaType,
+                        type,
+                        null,
+                        cancellationToken).ConfigureAwait(false);
 
                     result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
                     return true;
@@ -565,14 +572,14 @@ namespace MediaBrowser.Providers.Manager
 
                 try
                 {
-                    var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+                    using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
 
                     // If there's already an image of the same size, skip it
-                    if (response.ContentLength.HasValue)
+                    if (response.Content.Headers.ContentLength.HasValue)
                     {
                         try
                         {
-                            if (item.GetImages(imageType).Any(i => _fileSystem.GetFileInfo(i.Path).Length == response.ContentLength.Value))
+                            if (item.GetImages(imageType).Any(i => _fileSystem.GetFileInfo(i.Path).Length == response.Content.Headers.ContentLength.Value))
                             {
                                 response.Content.Dispose();
                                 continue;
@@ -584,7 +591,14 @@ namespace MediaBrowser.Providers.Manager
                         }
                     }
 
-                    await _providerManager.SaveImage(item, response.Content, response.ContentType, imageType, null, cancellationToken).ConfigureAwait(false);
+                    await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                    await _providerManager.SaveImage(
+                        item,
+                        stream,
+                        response.Content.Headers.ContentType.MediaType,
+                        imageType,
+                        null,
+                        cancellationToken).ConfigureAwait(false);
                     result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
                 }
                 catch (HttpException ex)

+ 21 - 15
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -5,6 +5,7 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
@@ -44,7 +45,7 @@ namespace MediaBrowser.Providers.Manager
     {
         private readonly object _refreshQueueLock = new object();
         private readonly ILogger<ProviderManager> _logger;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
@@ -66,7 +67,7 @@ namespace MediaBrowser.Providers.Manager
         /// <summary>
         /// Initializes a new instance of the <see cref="ProviderManager"/> class.
         /// </summary>
-        /// <param name="httpClient">The Http client.</param>
+        /// <param name="httpClientFactory">The Http client factory.</param>
         /// <param name="subtitleManager">The subtitle manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="libraryMonitor">The library monitor.</param>
@@ -75,7 +76,7 @@ namespace MediaBrowser.Providers.Manager
         /// <param name="appPaths">The server application paths.</param>
         /// <param name="libraryManager">The library manager.</param>
         public ProviderManager(
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ISubtitleManager subtitleManager,
             IServerConfigurationManager configurationManager,
             ILibraryMonitor libraryMonitor,
@@ -85,7 +86,7 @@ namespace MediaBrowser.Providers.Manager
             ILibraryManager libraryManager)
         {
             _logger = logger;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _configurationManager = configurationManager;
             _libraryMonitor = libraryMonitor;
             _fileSystem = fileSystem;
@@ -155,25 +156,23 @@ namespace MediaBrowser.Providers.Manager
         /// <inheritdoc/>
         public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         {
-            using var response = await _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url,
-                BufferContent = false
-            }).ConfigureAwait(false);
+            var httpClient = _httpClientFactory.CreateClient();
+            using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+
+            var contentType = response.Content.Headers.ContentType.MediaType;
 
             // Workaround for tvheadend channel icons
             // TODO: Isolate this hack into the tvh plugin
-            if (string.IsNullOrEmpty(response.ContentType))
+            if (string.IsNullOrEmpty(contentType))
             {
                 if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    response.ContentType = "image/png";
+                    contentType = "image/png";
                 }
             }
 
             // thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
-            if (response.ContentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
+            if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
             {
                 throw new HttpException("Invalid image received.")
                 {
@@ -181,7 +180,14 @@ namespace MediaBrowser.Providers.Manager
                 };
             }
 
-            await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            await SaveImage(
+                item,
+                stream,
+                contentType,
+                type,
+                imageIndex,
+                cancellationToken).ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
@@ -888,7 +894,7 @@ namespace MediaBrowser.Providers.Manager
         }
 
         /// <inheritdoc/>
-        public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
         {
             var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
 

+ 2 - 1
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -18,6 +18,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.2" />
     <PackageReference Include="TvDbSharper" Version="3.2.1" />
@@ -27,7 +28,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

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

@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.Providers.MediaInfo
 {

+ 7 - 10
MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -17,13 +17,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbAlbumImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _json;
 
-        public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IJsonSerializer json)
+        public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IJsonSerializer json)
         {
             _config = config;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _json = json;
         }
 
@@ -94,13 +94,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
         }
 
         /// <inheritdoc />
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            var httpClient = _httpClientFactory.CreateClient();
+            return httpClient.GetAsync(url, cancellationToken);
         }
 
         /// <inheritdoc />

+ 8 - 17
MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs

@@ -10,7 +10,6 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
@@ -26,16 +25,16 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     {
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _json;
 
         public static AudioDbAlbumProvider Current;
 
-        public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json)
+        public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
         {
             _config = config;
             _fileSystem = fileSystem;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _json = json;
 
             Current = this;
@@ -174,18 +173,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
-            using (var httpResponse = await _httpClient.SendAsync(
-                new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var response = httpResponse.Content)
-            using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
-            {
-                await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
-            }
+            using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
         }
 
         private static string GetAlbumDataPath(IApplicationPaths appPaths, string musicBrainzReleaseGroupId)
@@ -294,7 +285,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
         }
 
         /// <inheritdoc />
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }

+ 7 - 10
MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -17,14 +17,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbArtistImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _json;
 
-        public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClient httpClient)
+        public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClientFactory httpClientFactory)
         {
             _config = config;
             _json = json;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         /// <inheritdoc />
@@ -135,13 +135,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
             return list;
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            var httpClient = _httpClientFactory.CreateClient();
+            return httpClient.GetAsync(url, cancellationToken);
         }
 
         /// <inheritdoc />

+ 10 - 21
MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs

@@ -9,7 +9,6 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
@@ -25,7 +24,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     {
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _json;
 
         public static AudioDbArtistProvider Current;
@@ -33,11 +32,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
         private const string ApiKey = "195003";
         public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey;
 
-        public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json)
+        public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
         {
             _config = config;
             _fileSystem = fileSystem;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _json = json;
             Current = this;
         }
@@ -155,23 +154,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
             var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
 
-            using (var httpResponse = await _httpClient.SendAsync(
-                new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken,
-                    BufferContent = true
-                },
-                HttpMethod.Get).ConfigureAwait(false))
-            using (var response = httpResponse.Content)
-            {
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
+            using var response = await _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
 
-                using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
-                {
-                    await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
-                }
-            }
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+            await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -289,7 +278,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
         }
 
         /// <inheritdoc />
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }

+ 94 - 119
MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs

@@ -8,12 +8,12 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
 using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Music
 
         internal static MusicBrainzAlbumProvider Current;
 
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IApplicationHost _appHost;
         private readonly ILogger<MusicBrainzAlbumProvider> _logger;
 
@@ -51,11 +51,11 @@ namespace MediaBrowser.Providers.Music
         private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
 
         public MusicBrainzAlbumProvider(
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IApplicationHost appHost,
             ILogger<MusicBrainzAlbumProvider> logger)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _logger = logger;
 
@@ -123,11 +123,9 @@ namespace MediaBrowser.Providers.Music
 
             if (!string.IsNullOrWhiteSpace(url))
             {
-                using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-                using (var stream = response.Content)
-                {
-                    return GetResultsFromResponse(stream);
-                }
+                using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                return GetResultsFromResponse(stream);
             }
 
             return Enumerable.Empty<RemoteSearchResult>();
@@ -282,23 +280,19 @@ namespace MediaBrowser.Providers.Music
                 WebUtility.UrlEncode(albumName),
                 artistId);
 
-            using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var oReader = new StreamReader(stream, Encoding.UTF8))
+            using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var oReader = new StreamReader(stream, Encoding.UTF8);
+            var settings = new XmlReaderSettings
             {
-                var settings = new XmlReaderSettings()
-                {
-                    ValidationType = ValidationType.None,
-                    CheckCharacters = false,
-                    IgnoreProcessingInstructions = true,
-                    IgnoreComments = true
-                };
+                ValidationType = ValidationType.None,
+                CheckCharacters = false,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
 
-                using (var reader = XmlReader.Create(oReader, settings))
-                {
-                    return ReleaseResult.Parse(reader).FirstOrDefault();
-                }
-            }
+            using var reader = XmlReader.Create(oReader, settings);
+            return ReleaseResult.Parse(reader).FirstOrDefault();
         }
 
         private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
@@ -309,23 +303,19 @@ namespace MediaBrowser.Providers.Music
                 WebUtility.UrlEncode(albumName),
                 WebUtility.UrlEncode(artistName));
 
-            using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var oReader = new StreamReader(stream, Encoding.UTF8))
+            using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var oReader = new StreamReader(stream, Encoding.UTF8);
+            var settings = new XmlReaderSettings()
             {
-                var settings = new XmlReaderSettings()
-                {
-                    ValidationType = ValidationType.None,
-                    CheckCharacters = false,
-                    IgnoreProcessingInstructions = true,
-                    IgnoreComments = true
-                };
+                ValidationType = ValidationType.None,
+                CheckCharacters = false,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
 
-                using (var reader = XmlReader.Create(oReader, settings))
-                {
-                    return ReleaseResult.Parse(reader).FirstOrDefault();
-                }
-            }
+            using var reader = XmlReader.Create(oReader, settings);
+            return ReleaseResult.Parse(reader).FirstOrDefault();
         }
 
         private class ReleaseResult
@@ -624,30 +614,21 @@ namespace MediaBrowser.Providers.Music
         {
             var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture);
 
-            using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var oReader = new StreamReader(stream, Encoding.UTF8))
+            using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var oReader = new StreamReader(stream, Encoding.UTF8);
+            var settings = new XmlReaderSettings
             {
-                var settings = new XmlReaderSettings()
-                {
-                    ValidationType = ValidationType.None,
-                    CheckCharacters = false,
-                    IgnoreProcessingInstructions = true,
-                    IgnoreComments = true
-                };
-
-                using (var reader = XmlReader.Create(oReader, settings))
-                {
-                    var result = ReleaseResult.Parse(reader).FirstOrDefault();
+                ValidationType = ValidationType.None,
+                CheckCharacters = false,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
 
-                    if (result != null)
-                    {
-                        return result.ReleaseId;
-                    }
-                }
-            }
+            using var reader = XmlReader.Create(oReader, settings);
+            var result = ReleaseResult.Parse(reader).FirstOrDefault();
 
-            return null;
+            return result?.ReleaseId;
         }
 
         /// <summary>
@@ -660,59 +641,57 @@ namespace MediaBrowser.Providers.Music
         {
             var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture);
 
-            using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-            using (var stream = response.Content)
-            using (var oReader = new StreamReader(stream, Encoding.UTF8))
+            using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var oReader = new StreamReader(stream, Encoding.UTF8);
+            var settings = new XmlReaderSettings
             {
-                var settings = new XmlReaderSettings()
-                {
-                    ValidationType = ValidationType.None,
-                    CheckCharacters = false,
-                    IgnoreProcessingInstructions = true,
-                    IgnoreComments = true
-                };
+                ValidationType = ValidationType.None,
+                CheckCharacters = false,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
 
-                using (var reader = XmlReader.Create(oReader, settings))
-                {
-                    reader.MoveToContent();
-                    reader.Read();
+            using (var reader = XmlReader.Create(oReader, settings))
+            {
+                reader.MoveToContent();
+                reader.Read();
 
-                    // Loop through each element
-                    while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+                // Loop through each element
+                while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+                {
+                    if (reader.NodeType == XmlNodeType.Element)
                     {
-                        if (reader.NodeType == XmlNodeType.Element)
+                        switch (reader.Name)
                         {
-                            switch (reader.Name)
+                            case "release-group-list":
                             {
-                                case "release-group-list":
-                                    {
-                                        if (reader.IsEmptyElement)
-                                        {
-                                            reader.Read();
-                                            continue;
-                                        }
+                                if (reader.IsEmptyElement)
+                                {
+                                    reader.Read();
+                                    continue;
+                                }
 
-                                        using (var subReader = reader.ReadSubtree())
-                                        {
-                                            return GetFirstReleaseGroupId(subReader);
-                                        }
-                                    }
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    return GetFirstReleaseGroupId(subReader);
+                                }
+                            }
 
-                                default:
-                                    {
-                                        reader.Skip();
-                                        break;
-                                    }
+                            default:
+                            {
+                                reader.Skip();
+                                break;
                             }
                         }
-                        else
-                        {
-                            reader.Read();
-                        }
                     }
-
-                    return null;
+                    else
+                    {
+                        reader.Read();
+                    }
                 }
+
+                return null;
             }
         }
 
@@ -755,23 +734,19 @@ namespace MediaBrowser.Providers.Music
         /// A number of retries shall be made in order to try and satisfy the request before
         /// giving up and returning null.
         /// </summary>
-        internal async Task<HttpResponseInfo> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
+        internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
         {
-            var options = new HttpRequestOptions
-            {
-                Url = _musicBrainzBaseUrl.TrimEnd('/') + url,
-                CancellationToken = cancellationToken,
-                // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
-                // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
-                UserAgent = string.Format(
-                    CultureInfo.InvariantCulture,
-                    "{0} ( {1} )",
-                    _appHost.ApplicationUserAgent,
-                    _appHost.ApplicationUserAgentAddress),
-                BufferContent = false
-            };
+            using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url);
+
+            // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
+            // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
+            options.Headers.UserAgent.Add(new ProductInfoHeaderValue(string.Format(
+                CultureInfo.InvariantCulture,
+                "{0} ( {1} )",
+                _appHost.ApplicationUserAgent,
+                _appHost.ApplicationUserAgentAddress)));
 
-            HttpResponseInfo response;
+            HttpResponseMessage response;
             var attempts = 0u;
 
             do
@@ -790,7 +765,7 @@ namespace MediaBrowser.Providers.Music
                 _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
                 _stopWatchMusicBrainz.Restart();
 
-                response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+                response = await _httpClientFactory.CreateClient().SendAsync(options).ConfigureAwait(false);
 
                 // We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
             }
@@ -799,14 +774,14 @@ namespace MediaBrowser.Providers.Music
             // Log error if unable to query MB database due to throttling
             if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
             {
-                _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.Url);
+                _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri);
             }
 
             return response;
         }
 
         /// <inheritdoc />
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }

+ 9 - 15
MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs

@@ -6,11 +6,11 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Providers;
@@ -37,11 +37,9 @@ namespace MediaBrowser.Providers.Music
             {
                 var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture);
 
-                using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-                using (var stream = response.Content)
-                {
-                    return GetResultsFromResponse(stream);
-                }
+                using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                return GetResultsFromResponse(stream);
             }
             else
             {
@@ -51,7 +49,7 @@ namespace MediaBrowser.Providers.Music
                 var url = string.Format("/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch));
 
                 using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-                using (var stream = response.Content)
+                await using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                 {
                     var results = GetResultsFromResponse(stream).ToList();
 
@@ -66,13 +64,9 @@ namespace MediaBrowser.Providers.Music
                     // Try again using the search with accent characters url
                     url = string.Format("/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
 
-                    using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
-                    {
-                        using (var stream = response.Content)
-                        {
-                            return GetResultsFromResponse(stream);
-                        }
-                    }
+                    using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
+                    await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                    return GetResultsFromResponse(stream);
                 }
             }
 
@@ -298,7 +292,7 @@ namespace MediaBrowser.Providers.Music
 
         public string Name => "MusicBrainz";
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }

+ 7 - 7
MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs

@@ -1,10 +1,10 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
     public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly OmdbItemProvider _itemProvider;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
@@ -28,17 +28,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         public OmdbEpisodeProvider(
             IJsonSerializer jsonSerializer,
             IApplicationHost appHost,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager,
             IFileSystem fileSystem,
             IServerConfigurationManager configurationManager)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _appHost = appHost;
-            _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager);
+            _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
         }
 
         // After TheTvDb
@@ -69,7 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             {
                 if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
                 {
-                    result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager)
+                    result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager)
                         .FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
                 }
             }
@@ -77,7 +77,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             return result;
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _itemProvider.GetImageResponse(url, cancellationToken);
         }

+ 7 - 11
MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs

@@ -1,10 +1,10 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -19,16 +19,16 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 {
     public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IApplicationHost _appHost;
 
-        public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+        public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _appHost = appHost;
@@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             var list = new List<RemoteImageInfo>();
 
-            var provider = new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager);
+            var provider = new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager);
 
             if (!string.IsNullOrWhiteSpace(imdbId))
             {
@@ -79,13 +79,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             return list;
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
 
         public string Name => "The Open Movie Database";

+ 61 - 69
MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs

@@ -5,10 +5,10 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
@@ -35,13 +35,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         public OmdbItemProvider(
             IJsonSerializer jsonSerializer,
             IApplicationHost appHost,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager,
             IFileSystem fileSystem,
             IServerConfigurationManager configurationManager)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
@@ -129,67 +129,63 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             var url = OmdbProvider.GetOmdbUrl(urlQuery, _appHost, cancellationToken);
 
-            using (var response = await OmdbProvider.GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
+            using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var resultList = new List<SearchResult>();
+
+            if (isSearch)
             {
-                using (var stream = response.Content)
+                var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false);
+                if (searchResultList != null && searchResultList.Search != null)
                 {
-                    var resultList = new List<SearchResult>();
-
-                    if (isSearch)
-                    {
-                        var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false);
-                        if (searchResultList != null && searchResultList.Search != null)
-                        {
-                            resultList.AddRange(searchResultList.Search);
-                        }
-                    }
-                    else
-                    {
-                        var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
-                        if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
-                        {
-                            resultList.Add(result);
-                        }
-                    }
-
-                    return resultList.Select(result =>
-                    {
-                        var item = new RemoteSearchResult
-                        {
-                            IndexNumber = searchInfo.IndexNumber,
-                            Name = result.Title,
-                            ParentIndexNumber = searchInfo.ParentIndexNumber,
-                            SearchProviderName = Name
-                        };
-
-                        if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue)
-                        {
-                            item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value;
-                        }
-
-                        item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
-
-                        if (result.Year.Length > 0
-                            && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
-                        {
-                            item.ProductionYear = parsedYear;
-                        }
-
-                        if (!string.IsNullOrEmpty(result.Released)
-                            && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
-                        {
-                            item.PremiereDate = released;
-                        }
-
-                        if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase))
-                        {
-                            item.ImageUrl = result.Poster;
-                        }
-
-                        return item;
-                    });
+                    resultList.AddRange(searchResultList.Search);
                 }
             }
+            else
+            {
+                var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
+                if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
+                {
+                    resultList.Add(result);
+                }
+            }
+
+            return resultList.Select(result =>
+            {
+                var item = new RemoteSearchResult
+                {
+                    IndexNumber = searchInfo.IndexNumber,
+                    Name = result.Title,
+                    ParentIndexNumber = searchInfo.ParentIndexNumber,
+                    SearchProviderName = Name
+                };
+
+                if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue)
+                {
+                    item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value;
+                }
+
+                item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
+
+                if (result.Year.Length > 0
+                    && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
+                {
+                    item.ProductionYear = parsedYear;
+                }
+
+                if (!string.IsNullOrEmpty(result.Released)
+                    && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
+                {
+                    item.PremiereDate = released;
+                }
+
+                if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.ImageUrl = result.Poster;
+                }
+
+                return item;
+            });
         }
 
         public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
@@ -224,7 +220,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                 result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
                 result.HasMetadata = true;
 
-                await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
             }
 
             return result;
@@ -256,7 +252,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                 result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
                 result.HasMetadata = true;
 
-                await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
             }
 
             return result;
@@ -276,13 +272,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             return first == null ? null : first.GetProviderId(MetadataProvider.Imdb);
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
 
         class SearchResult

+ 15 - 30
MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

@@ -10,7 +10,6 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -25,14 +24,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IApplicationHost _appHost;
 
-        public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+        public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _appHost = appHost;
@@ -293,15 +292,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             var url = GetOmdbUrl(string.Format("i={0}&plot=short&tomatoes=true&r=json", imdbParam), _appHost, cancellationToken);
 
-            using (var response = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
-            {
-                using (var stream = response.Content)
-                {
-                    var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
-                    Directory.CreateDirectory(Path.GetDirectoryName(path));
-                    _jsonSerializer.SerializeToFile(rootObject, path);
-                }
-            }
+            using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            _jsonSerializer.SerializeToFile(rootObject, path);
 
             return path;
         }
@@ -330,28 +325,18 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             var url = GetOmdbUrl(string.Format("i={0}&season={1}&detail=full", imdbParam, seasonId), _appHost, cancellationToken);
 
-            using (var response = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
-            {
-                using (var stream = response.Content)
-                {
-                    var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
-                    Directory.CreateDirectory(Path.GetDirectoryName(path));
-                    _jsonSerializer.SerializeToFile(rootObject, path);
-                }
-            }
+            using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            _jsonSerializer.SerializeToFile(rootObject, path);
 
             return path;
         }
 
-        public static Task<HttpResponseInfo> GetOmdbResponse(IHttpClient httpClient, string url, CancellationToken cancellationToken)
+        public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken)
         {
-            return httpClient.SendAsync(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = true,
-                EnableDefaultUserAgent = true
-            }, HttpMethod.Get);
+            return httpClient.GetAsync(url, cancellationToken);
         }
 
         internal string GetDataFilePath(string imdbId)

+ 6 - 10
MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs

@@ -2,9 +2,9 @@
 
 using System;
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -18,13 +18,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 {
     public class TvdbEpisodeImageProvider : IRemoteImageProvider
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TvdbEpisodeImageProvider> _logger;
         private readonly TvdbClientManager _tvdbClientManager;
 
-        public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
+        public TvdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _tvdbClientManager = tvdbClientManager;
         }
@@ -113,13 +113,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs

@@ -2,9 +2,9 @@
 
 using System;
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -21,13 +21,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
     /// </summary>
     public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TvdbEpisodeProvider> _logger;
         private readonly TvdbClientManager _tvdbClientManager;
 
-        public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
+        public TvdbEpisodeProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _tvdbClientManager = tvdbClientManager;
         }
@@ -242,13 +242,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             return result;
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
 
         public int Order => 0;

+ 6 - 10
MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -20,15 +20,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 {
     public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TvdbPersonImageProvider> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly TvdbClientManager _tvdbClientManager;
 
-        public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
+        public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
         {
             _libraryManager = libraryManager;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _tvdbClientManager = tvdbClientManager;
         }
@@ -104,13 +104,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
         }
 
         /// <inheritdoc />
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -20,13 +20,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 {
     public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TvdbSeasonImageProvider> _logger;
         private readonly TvdbClientManager _tvdbClientManager;
 
-        public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
+        public TvdbSeasonImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _tvdbClientManager = tvdbClientManager;
         }
@@ -146,13 +146,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -20,13 +20,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 {
     public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TvdbSeriesImageProvider> _logger;
         private readonly TvdbClientManager _tvdbClientManager;
 
-        public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
+        public TvdbSeriesImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _tvdbClientManager = tvdbClientManager;
         }
@@ -144,13 +144,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 11
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs

@@ -3,11 +3,11 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -25,15 +25,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
     {
         internal static TvdbSeriesProvider Current { get; private set; }
 
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TvdbSeriesProvider> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localizationManager;
         private readonly TvdbClientManager _tvdbClientManager;
 
-        public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
+        public TvdbSeriesProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             _libraryManager = libraryManager;
             _localizationManager = localizationManager;
@@ -408,14 +408,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url,
-                BufferContent = false
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
@@ -20,11 +20,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 {
     public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public TmdbBoxSetImageProvider(IHttpClient httpClient)
+        public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         public string Name => ProviderName;
@@ -153,13 +153,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 20 - 32
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs

@@ -5,10 +5,11 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
@@ -36,7 +37,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
 
         public TmdbBoxSetProvider(
@@ -45,7 +46,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
             IServerConfigurationManager config,
             IFileSystem fileSystem,
             ILocalizationManager localization,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager)
         {
             _logger = logger;
@@ -53,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
             _config = config;
             _fileSystem = fileSystem;
             _localization = localization;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             Current = this;
         }
@@ -187,21 +188,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            CollectionResult mainResult;
-
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                using (var json = response.Content)
-                {
-                    mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false);
-                }
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
+            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+            await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false);
+
             cancellationToken.ThrowIfCancellationRequested();
 
             if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
@@ -216,18 +212,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
                         url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
                     }
 
-                    using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-                    {
-                        Url = url,
-                        CancellationToken = cancellationToken,
-                        AcceptHeader = TmdbUtils.AcceptHeader
-                    }).ConfigureAwait(false))
+                    using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+                    foreach (var header in TmdbUtils.AcceptHeaders)
                     {
-                        using (var json = response.Content)
-                        {
-                            mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false);
-                        }
+                        langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
                     }
+
+                    await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                    mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).ConfigureAwait(false);
                 }
             }
 
@@ -277,13 +269,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
             return dataPath;
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs

@@ -4,9 +4,9 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
@@ -23,13 +23,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
     public class TmdbImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IFileSystem _fileSystem;
 
-        public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem)
+        public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
         }
 
@@ -202,13 +202,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 36 - 71
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -6,11 +6,11 @@ using System.Globalization;
 using System.IO;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -18,7 +18,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
@@ -34,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         internal static TmdbMovieProvider Current { get; private set; }
 
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger<TmdbMovieProvider> _logger;
@@ -45,7 +44,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
         public TmdbMovieProvider(
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             IFileSystem fileSystem,
             IServerConfigurationManager configurationManager,
             ILogger<TmdbMovieProvider> logger,
@@ -53,7 +52,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             IApplicationHost appHost)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _logger = logger;
@@ -146,20 +145,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 return _tmdbSettings;
             }
 
-            using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey));
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                Url = string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey),
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
-            {
-                using (Stream json = response.Content)
-                {
-                    _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(json).ConfigureAwait(false);
-
-                    return _tmdbSettings;
-                }
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
+
+            using var response = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false);
+            return _tmdbSettings;
         }
 
         private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
@@ -331,42 +326,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 url += "&include_image_language=" + GetImageLanguagesParam(language);
             }
 
-            MovieResult mainResult;
-
             cancellationToken.ThrowIfCancellationRequested();
 
-            // Cache if not using a tmdbId because we won't have the tmdb cache directory structure. So use the lower level cache.
-            var cacheMode = isTmdbId ? CacheMode.None : CacheMode.Unconditional;
-            var cacheLength = TimeSpan.FromDays(3);
-
-            try
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                using (var response = await GetMovieDbResponse(new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken,
-                    AcceptHeader = TmdbUtils.AcceptHeader,
-                    CacheMode = cacheMode,
-                    CacheLength = cacheLength
-                }).ConfigureAwait(false))
-                {
-                    using (var json = response.Content)
-                    {
-                        mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
-                    }
-                }
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
-            catch (HttpException ex)
-            {
-                // Return null so that callers know there is no metadata for this id
-                if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
-                {
-                    return null;
-                }
 
-                throw;
+            using var mainResponse = await GetMovieDbResponse(requestMessage);
+            if (mainResponse.StatusCode == HttpStatusCode.NotFound)
+            {
+                return null;
             }
 
+            await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
+
             cancellationToken.ThrowIfCancellationRequested();
 
             // If the language preference isn't english, then have the overview fallback to english if it's blank
@@ -385,22 +361,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                     url += "&include_image_language=" + GetImageLanguagesParam(language);
                 }
 
-                using (var response = await GetMovieDbResponse(new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken,
-                    AcceptHeader = TmdbUtils.AcceptHeader,
-                    CacheMode = cacheMode,
-                    CacheLength = cacheLength
-                }).ConfigureAwait(false))
+                using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+                foreach (var header in TmdbUtils.AcceptHeaders)
                 {
-                    using (var json = response.Content)
-                    {
-                        var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
-
-                        mainResult.Overview = englishResult.Overview;
-                    }
+                    langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
                 }
+
+                using var langResponse = await GetMovieDbResponse(langRequestMessage);
+
+                await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
+                mainResult.Overview = langResult.Overview;
             }
 
             return mainResult;
@@ -409,25 +380,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         /// <summary>
         /// Gets the movie db response.
         /// </summary>
-        internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options)
+        internal async Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message)
         {
-            options.BufferContent = true;
-            options.UserAgent = _appHost.ApplicationUserAgent;
-
-            return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+            message.Headers.UserAgent.Add(new ProductInfoHeaderValue(_appHost.ApplicationUserAgent));
+            return await _httpClientFactory.CreateClient().SendAsync(message);
         }
 
         /// <inheritdoc />
         public int Order => 1;
 
         /// <inheritdoc />
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 57 - 73
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs

@@ -5,10 +5,11 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -168,47 +169,38 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
             var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type);
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                Url = url3,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+            }
 
-            }).ConfigureAwait(false))
-            {
-                using (var json = response.Content)
-                {
-                    var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(json).ConfigureAwait(false);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).ConfigureAwait(false);
 
-                    var results = searchResults.Results ?? new List<MovieResult>();
+            var results = searchResults.Results ?? new List<MovieResult>();
 
-                    return results
-                        .Select(i =>
+            return results
+                .Select(i =>
+                {
+                    var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Title ?? i.Name ?? i.Original_Title, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path};
+
+                    if (!string.IsNullOrWhiteSpace(i.Release_Date))
+                    {
+                        // These dates are always in this exact format
+                        if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
                         {
-                            var remoteResult = new RemoteSearchResult
-                            {
-                                SearchProviderName = TmdbMovieProvider.Current.Name,
-                                Name = i.Title ?? i.Name ?? i.Original_Title,
-                                ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
-                            };
-
-                            if (!string.IsNullOrWhiteSpace(i.Release_Date))
-                            {
-                                // These dates are always in this exact format
-                                if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
-                                {
-                                    remoteResult.PremiereDate = r.ToUniversalTime();
-                                    remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
-                                }
-                            }
-
-                            remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
-                            return remoteResult;
-                        })
-                        .ToList();
-                }
-            }
+                            remoteResult.PremiereDate = r.ToUniversalTime();
+                            remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+                        }
+                    }
+
+                    remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+                    return remoteResult;
+                })
+                .ToList();
         }
 
         private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
@@ -220,46 +212,38 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
             var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv");
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url3,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
+            var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                using (var json = response.Content)
-                {
-                    var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(json).ConfigureAwait(false);
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+            }
+
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).ConfigureAwait(false);
 
-                    var results = searchResults.Results ?? new List<TvResult>();
+            var results = searchResults.Results ?? new List<TvResult>();
 
-                    return results
-                        .Select(i =>
+            return results
+                .Select(i =>
+                {
+                    var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Name ?? i.Original_Name, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path};
+
+                    if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
+                    {
+                        // These dates are always in this exact format
+                        if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
                         {
-                            var remoteResult = new RemoteSearchResult
-                            {
-                                SearchProviderName = TmdbMovieProvider.Current.Name,
-                                Name = i.Name ?? i.Original_Name,
-                                ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
-                            };
-
-                            if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
-                            {
-                                // These dates are always in this exact format
-                                if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
-                                {
-                                    remoteResult.PremiereDate = r.ToUniversalTime();
-                                    remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
-                                }
-                            }
-
-                            remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
-                            return remoteResult;
-                        })
-                        .ToList();
-                }
-            }
+                            remoteResult.PremiereDate = r.ToUniversalTime();
+                            remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+                        }
+                    }
+
+                    remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+                    return remoteResult;
+                })
+                .ToList();
         }
     }
 }

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs

@@ -2,9 +2,9 @@
 
 using System;
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Providers;
@@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
 
         public string Name => TmdbMovieProvider.Current.Name;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
         }

+ 6 - 10
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -22,13 +22,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
     {
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient)
+        public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
         {
             _config = config;
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         public string Name => ProviderName;
@@ -127,13 +127,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 25 - 38
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs

@@ -6,11 +6,12 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -36,20 +37,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<TmdbPersonProvider> _logger;
 
         public TmdbPersonProvider(
             IFileSystem fileSystem,
             IServerConfigurationManager configurationManager,
             IJsonSerializer jsonSerializer,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILogger<TmdbPersonProvider> logger)
         {
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _logger = logger;
             Current = this;
         }
@@ -96,22 +97,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 
             var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey);
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+            }
 
-            }).ConfigureAwait(false))
-            {
-                using (var json = response.Content)
-                {
-                    var result = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(json).ConfigureAwait(false) ??
-                                 new TmdbSearchResult<PersonSearchResult>();
+            var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
 
-                    return result.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
-                }
-            }
+            var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false)
+                         ?? new TmdbSearchResult<PersonSearchResult>();
+
+            return result2.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
         }
 
         private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl)
@@ -230,23 +228,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 
             var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id);
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                using (var json = response.Content)
-                {
-                    Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
-                    using (var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
-                    {
-                        await json.CopyToAsync(fs).ConfigureAwait(false);
-                    }
-                }
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
+
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+            await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            await response.Content.CopyToAsync(fs).ConfigureAwait(false);
         }
 
         private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
@@ -266,13 +257,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             return Path.Combine(appPaths.CachePath, "tmdb-people");
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 4 - 4
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -26,8 +26,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             IRemoteImageProvider,
             IHasOrder
     {
-        public TmdbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
-            : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+        public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+            : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
         { }
 
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
@@ -115,7 +115,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return images.Stills ?? new List<Still>();
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return GetResponse(url, cancellationToken);
         }

+ 4 - 4
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs

@@ -5,9 +5,9 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -27,8 +27,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             IRemoteMetadataProvider<Episode, EpisodeInfo>,
             IHasOrder
     {
-        public TmdbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
-            : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+        public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+            : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
         { }
 
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
@@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return result;
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return GetResponse(url, cancellationToken);
         }

+ 14 - 20
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs

@@ -3,9 +3,10 @@
 using System;
 using System.Globalization;
 using System.IO;
+using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
@@ -19,16 +20,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     public abstract class TmdbEpisodeProviderBase
     {
         private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
         private readonly ILogger<TmdbEpisodeProviderBase> _logger;
 
-        protected TmdbEpisodeProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+        protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _configurationManager = configurationManager;
             _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
@@ -124,27 +125,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
-            {
-                using (var json = response.Content)
-                {
-                    return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(json).ConfigureAwait(false);
-                }
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
+
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false);
         }
 
-        protected Task<HttpResponseInfo> GetResponse(string url, CancellationToken cancellationToken)
+        protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs

@@ -4,9 +4,9 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -22,12 +22,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient)
+        public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         public int Order => 1;
@@ -36,13 +36,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         public static string ProviderName => TmdbUtils.ProviderName;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
 
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)

+ 14 - 20
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs

@@ -5,9 +5,10 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -26,7 +27,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
     {
         private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
@@ -35,9 +36,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         internal static TmdbSeasonProvider Current { get; private set; }
 
-        public TmdbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
+        public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _configurationManager = configurationManager;
             _fileSystem = fileSystem;
             _localization = localization;
@@ -121,13 +122,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
         }
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
 
         private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
@@ -215,18 +212,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                using (var json = response.Content)
-                {
-                    return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(json).ConfigureAwait(false);
-                }
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
+
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).ConfigureAwait(false);
         }
     }
 }

+ 6 - 10
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs

@@ -3,9 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -23,13 +23,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IFileSystem _fileSystem;
 
-        public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem)
+        public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
         {
             _jsonSerializer = jsonSerializer;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
         }
 
@@ -180,13 +180,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         // After tvdb and fanart
         public int Order => 2;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 53 - 64
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs

@@ -5,10 +5,11 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -35,7 +36,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger<TmdbSeriesProvider> _logger;
         private readonly ILocalizationManager _localization;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
@@ -48,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             IServerConfigurationManager configurationManager,
             ILogger<TmdbSeriesProvider> logger,
             ILocalizationManager localization,
-            IHttpClient httpClient,
+            IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager)
         {
             _jsonSerializer = jsonSerializer;
@@ -56,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             _configurationManager = configurationManager;
             _logger = logger;
             _localization = localization;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             Current = this;
         }
@@ -413,24 +414,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            SeriesResult mainResult;
-
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
+            using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                using (var json = response.Content)
-                {
-                    mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false);
+                mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+            }
 
-                    if (!string.IsNullOrEmpty(language))
-                    {
-                        mainResult.ResultLanguage = language;
-                    }
-                }
+            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage);
+            await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false);
+
+            if (!string.IsNullOrEmpty(language))
+            {
+                mainResult.ResultLanguage = language;
             }
 
             cancellationToken.ThrowIfCancellationRequested();
@@ -451,21 +447,18 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
                 }
 
-                using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken,
-                    AcceptHeader = TmdbUtils.AcceptHeader
-                }).ConfigureAwait(false))
+                using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+                foreach (var header in TmdbUtils.AcceptHeaders)
                 {
-                    using (var json = response.Content)
-                    {
-                        var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false);
-
-                        mainResult.Overview = englishResult.Overview;
-                        mainResult.ResultLanguage = "en";
-                    }
+                    mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
                 }
+
+                using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+                await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false);
+
+                mainResult.Overview = englishResult.Overview;
+                mainResult.ResultLanguage = "en";
             }
 
             return mainResult;
@@ -515,38 +508,38 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 TmdbUtils.ApiKey,
                 externalSource);
 
-            using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+            foreach (var header in TmdbUtils.AcceptHeaders)
             {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = TmdbUtils.AcceptHeader
-            }).ConfigureAwait(false))
+                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+            }
+
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+            await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+            var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false);
+
+            if (result != null && result.Tv_Results != null)
             {
-                using (var json = response.Content)
+                var tv = result.Tv_Results.FirstOrDefault();
+
+                if (tv != null)
                 {
-                    var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(json).ConfigureAwait(false);
+                    var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+                    var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
 
-                    if (result != null && result.Tv_Results != null)
+                    var remoteResult = new RemoteSearchResult
                     {
-                        var tv = result.Tv_Results.FirstOrDefault();
-
-                        if (tv != null)
-                        {
-                            var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-                            var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
-                            var remoteResult = new RemoteSearchResult
-                            {
-                                Name = tv.Name,
-                                SearchProviderName = Name,
-                                ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path) ? null : tmdbImageUrl + tv.Poster_Path
-                            };
+                        Name = tv.Name,
+                        SearchProviderName = Name,
+                        ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path)
+                            ? null
+                            : tmdbImageUrl + tv.Poster_Path
+                    };
 
-                            remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
+                    remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
 
-                            return remoteResult;
-                        }
-                    }
+                    return remoteResult;
                 }
             }
 
@@ -556,13 +549,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         // After TheTVDB
         public int Order => 1;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 2 - 1
MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Net.Mime;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
 
@@ -32,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <summary>
         /// Value of the Accept header for requests to the provider.
         /// </summary>
-        public const string AcceptHeader = "application/json,image/*";
+        public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" };
 
         /// <summary>
         /// Maps the TMDB provided roles for crew members to Jellyfin roles.

+ 6 - 10
MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Providers;
@@ -13,11 +13,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
 {
     public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo>
     {
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
-        public TmdbTrailerProvider(IHttpClient httpClient)
+        public TmdbTrailerProvider(IHttpClientFactory httpClientFactory)
         {
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
@@ -34,13 +34,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url
-            });
+            return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
         }
     }
 }

+ 14 - 28
MediaBrowser.Providers/Studios/StudiosImageProvider.cs

@@ -7,7 +7,6 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
@@ -20,13 +19,13 @@ namespace MediaBrowser.Providers.Studios
     public class StudiosImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
         private readonly IFileSystem _fileSystem;
 
-        public StudiosImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+        public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
         {
             _config = config;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
             _fileSystem = fileSystem;
         }
 
@@ -108,26 +107,22 @@ namespace MediaBrowser.Providers.Studios
         {
             const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studiothumbs.txt";
 
-            return EnsureList(url, file, _httpClient, _fileSystem, cancellationToken);
+            return EnsureList(url, file, _fileSystem, cancellationToken);
         }
 
         private Task<string> EnsurePosterList(string file, CancellationToken cancellationToken)
         {
             const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studioposters.txt";
 
-            return EnsureList(url, file, _httpClient, _fileSystem, cancellationToken);
+            return EnsureList(url, file, _fileSystem, cancellationToken);
         }
 
         public int Order => 0;
 
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url,
-                BufferContent = false
-            });
+            var httpClient = _httpClientFactory.CreateClient();
+            return httpClient.GetAsync(url, cancellationToken);
         }
 
         /// <summary>
@@ -135,30 +130,21 @@ namespace MediaBrowser.Providers.Studios
         /// </summary>
         /// <param name="url">The URL.</param>
         /// <param name="file">The file.</param>
-        /// <param name="httpClient">The HTTP client.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        public async Task<string> EnsureList(string url, string file, IHttpClient httpClient, IFileSystem fileSystem, CancellationToken cancellationToken)
+        public async Task<string> EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken)
         {
             var fileInfo = fileSystem.GetFileInfo(file);
 
             if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1)
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(file));
+                var httpClient = _httpClientFactory.CreateClient();
 
-                using (var res = await httpClient.SendAsync(
-                    new HttpRequestOptions
-                    {
-                        CancellationToken = cancellationToken,
-                        Url = url
-                    },
-                    HttpMethod.Get).ConfigureAwait(false))
-                using (var content = res.Content)
-                using (var fileStream = new FileStream(file, FileMode.Create))
-                {
-                    await content.CopyToAsync(fileStream).ConfigureAwait(false);
-                }
+                Directory.CreateDirectory(Path.GetDirectoryName(file));
+                await using var response = await httpClient.GetStreamAsync(url).ConfigureAwait(false);
+                await using var fileStream = new FileStream(file, FileMode.Create);
+                await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
             }
 
             return file;