Merge branch 'master' into event-rewrite-1

This commit is contained in:
Patrick Barron 2020-08-19 21:45:31 +00:00 committed by Patrick Barron
commit 98ed90c4a2
74 changed files with 2348 additions and 2275 deletions

View file

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

View file

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

View file

@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory; _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 /> /// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) 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); 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) private static void SetBookProperties(BaseItemDto dto, Book item)
{ {
dto.SeriesName = item.SeriesName; 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) private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
{ {
if (!string.IsNullOrEmpty(item.Album)) if (!string.IsNullOrEmpty(item.Album))
@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
.ToArray(); .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) private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{ {
try try

View file

@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
public class LiveTvMediaSourceProvider : IMediaSourceProvider public class LiveTvMediaSourceProvider : IMediaSourceProvider
{ {
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. // 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 char StreamIdDelimiter = '_';
private const string StreamIdDelimeterString = "_";
private readonly ILiveTvManager _liveTvManager; private readonly ILiveTvManager _liveTvManager;
private readonly ILogger<LiveTvMediaSourceProvider> _logger; 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) 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.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 // Dummy this up so that direct play checks can still run
@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <inheritdoc /> /// <inheritdoc />
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) 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 mediaSourceId = keys.Length >= 3 ? keys[2] : null;
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);

View file

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

View file

@ -7,8 +7,8 @@
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru", "Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}", "LabelIpAddressValue": "Alamat IP: {0}",
"ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan", "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
"ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan", "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisan", "Inherit": "Warisan",
"HomeVideos": "Video Rumah", "HomeVideos": "Video Rumah",
"HeaderRecordingGroups": "Grup Rekaman", "HeaderRecordingGroups": "Grup Rekaman",
@ -19,8 +19,8 @@
"HeaderFavoriteEpisodes": "Episode Favorit", "HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit", "HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit", "HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Masih Melihat", "HeaderContinueWatching": "Lanjutkan Menonton",
"HeaderCameraUploads": "Uplod Kamera", "HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis", "HeaderAlbumArtists": "Album Artis",
"Genres": "Genre", "Genres": "Genre",
"Folders": "Folder", "Folders": "Folder",
@ -32,11 +32,11 @@
"ChapterNameValue": "Bagian {0}", "ChapterNameValue": "Bagian {0}",
"Channels": "Saluran", "Channels": "Saluran",
"TvShows": "Seri TV", "TvShows": "Seri TV",
"SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}", "SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
"StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.", "StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
"Songs": "Lagu", "Songs": "Lagu",
"Playlists": "Daftar putar", "Playlists": "Daftar putar",
"NotificationOptionPluginUninstalled": "Plugin dilepas", "NotificationOptionPluginUninstalled": "Plugin dihapus",
"MusicVideos": "Video musik", "MusicVideos": "Video musik",
"VersionNumber": "Versi {0}", "VersionNumber": "Versi {0}",
"ValueSpecialEpisodeName": "Spesial - {0}", "ValueSpecialEpisodeName": "Spesial - {0}",
@ -65,7 +65,7 @@
"Photos": "Foto", "Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci", "NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal", "NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
"NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan", "NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang", "NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
"NotificationOptionPluginInstalled": "Plugin terpasang", "NotificationOptionPluginInstalled": "Plugin terpasang",
"NotificationOptionPluginError": "Kegagalan plugin", "NotificationOptionPluginError": "Kegagalan plugin",
@ -74,14 +74,14 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah", "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang", "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia", "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", "NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}", "NameSeasonNumber": "Musim {0}",
"NameInstallFailed": "{0} instalasi gagal", "NameInstallFailed": "{0} penginstalan gagal",
"Music": "Musik", "Music": "Musik",
"Movies": "Film", "Movies": "Film",
"MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui", "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}", "FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}", "CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus", "DeviceOfflineWithName": "{0} telah terputus",
@ -90,6 +90,28 @@
"NotificationOptionVideoPlayback": "Pemutaran video dimulai", "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti", "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
"NotificationOptionAudioPlayback": "Pemutaran audio dimulai", "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
"MixedContent": "Konten campur", "MixedContent": "Konten campuran",
"PluginUninstalledWithName": "{0} telah dihapus" "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"
} }

View file

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

View file

@ -45,7 +45,7 @@
"TvShows": "தொலைக்காட்சித் தொடர்கள்", "TvShows": "தொலைக்காட்சித் தொடர்கள்",
"Sync": "ஒத்திசைவு", "Sync": "ஒத்திசைவு",
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.", "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
"Songs": "பாடடுகள்", "Songs": "பாட்கள்",
"Shows": "தொடர்கள்", "Shows": "தொடர்கள்",
"ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்", "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
"ScheduledTaskStartedWithName": "{0} துவங்கியது", "ScheduledTaskStartedWithName": "{0} துவங்கியது",
@ -97,5 +97,21 @@
"Application": "செயலி", "Application": "செயலி",
"Albums": "ஆல்பங்கள்", "Albums": "ஆல்பங்கள்",
"NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.", "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} ஐ இயக்குகிறது"
} }

View file

@ -1,93 +1,32 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos; 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.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers namespace Jellyfin.Api.Controllers
{ {
/// <summary> /// <summary>
/// The audio controller. /// The audio controller.
/// </summary> /// </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 public class AudioController : BaseJellyfinApiController
{ {
private readonly IDlnaManager _dlnaManager; private readonly AudioHelper _audioHelper;
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 TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AudioController"/> class. /// Initializes a new instance of the <see cref="AudioController"/> class.
/// </summary> /// </summary>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param> public AudioController(AudioHelper audioHelper)
/// <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)
{ {
_dlnaManager = dlnaManager; _audioHelper = audioHelper;
_authContext = authorizationContext;
_userManager = userManger;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
} }
/// <summary> /// <summary>
@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] EncodingContext? context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string>? streamOptions) [FromQuery] Dictionary<string, string>? streamOptions)
{ {
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
StreamingRequestDto streamingRequest = new StreamingRequestDto StreamingRequestDto streamingRequest = new StreamingRequestDto
{ {
Id = itemId, Id = itemId,
@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
using var state = await StreamingHelpers.GetStreamingState( return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
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);
} }
} }
} }

View file

@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet] [HttpGet]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [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 }; var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
return _deviceManager.GetDevices(deviceQuery); return _deviceManager.GetDevices(deviceQuery);

View file

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

View file

@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager; private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsController> _logger; private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; 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="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> 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="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="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
public DynamicHlsController( public DynamicHlsController(
ILibraryManager libraryManager, ILibraryManager libraryManager,
IUserManager userManager, IUserManager userManager,
@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers
IConfiguration configuration, IConfiguration configuration,
IDeviceManager deviceManager, IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper, TranscodingJobHelper transcodingJobHelper,
INetworkManager networkManager, ILogger<DynamicHlsController> logger,
ILogger<DynamicHlsController> logger) DynamicHlsHelper dynamicHlsHelper)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers
_configuration = configuration; _configuration = configuration;
_deviceManager = deviceManager; _deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper; _transcodingJobHelper = transcodingJobHelper;
_networkManager = networkManager;
_logger = logger; _logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
} }
@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true)
{ {
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new HlsVideoRequestDto var streamingRequest = new HlsVideoRequestDto
{ {
Id = itemId, Id = itemId,
@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource) return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
.ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true)
{ {
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new HlsAudioRequestDto var streamingRequest = new HlsAudioRequestDto
{ {
Id = itemId, Id = itemId,
@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource) return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
.ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false); .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) private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
{ {
using var state = await StreamingHelpers.GetStreamingState( 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); 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) private double[] GetSegmentLengths(StreamState state)
{ {
var result = new List<double>(); var result = new List<double>();
@ -2089,7 +1657,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask; 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) private long GetEndPositionTicks(StreamState state, int requestedIndex)

View file

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

View file

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

View file

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

View file

@ -1,30 +1,18 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.MediaInfoDtos;
using Jellyfin.Api.Models.VideoDtos; 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.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IDeviceManager _deviceManager; private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly INetworkManager _networkManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IUserManager _userManager;
private readonly IAuthorizationContext _authContext; private readonly IAuthorizationContext _authContext;
private readonly ILogger<MediaInfoController> _logger; private readonly ILogger<MediaInfoController> _logger;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly MediaInfoHelper _mediaInfoHelper;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class. /// 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="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> 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="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="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> 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( public MediaInfoController(
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager, IDeviceManager deviceManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
INetworkManager networkManager,
IMediaEncoder mediaEncoder,
IUserManager userManager,
IAuthorizationContext authContext, IAuthorizationContext authContext,
ILogger<MediaInfoController> logger, ILogger<MediaInfoController> logger,
IServerConfigurationManager serverConfigurationManager) MediaInfoHelper mediaInfoHelper)
{ {
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager; _deviceManager = deviceManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_networkManager = networkManager;
_mediaEncoder = mediaEncoder;
_userManager = userManager;
_authContext = authContext; _authContext = authContext;
_logger = logger; _logger = logger;
_serverConfigurationManager = serverConfigurationManager; _mediaInfoHelper = mediaInfoHelper;
} }
/// <summary> /// <summary>
@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId) 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> /// <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) if (profile != null)
{ {
@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers
foreach (var mediaSource in info.MediaSources) foreach (var mediaSource in info.MediaSources)
{ {
SetDeviceSpecificData( _mediaInfoHelper.SetDeviceSpecificData(
item, item,
mediaSource, mediaSource,
profile, profile,
@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers
enableDirectStream, enableDirectStream,
enableTranscoding, enableTranscoding,
allowVideoStreamCopy, allowVideoStreamCopy,
allowAudioStreamCopy); allowAudioStreamCopy,
Request.HttpContext.Connection.RemoteIpAddress.ToString());
} }
SortMediaSources(info, maxStreamingBitrate); _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
} }
if (autoOpenLiveStream) if (autoOpenLiveStream)
@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{ {
var openStreamResult = await OpenMediaSource(new LiveStreamRequest var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
{ Request,
AudioStreamIndex = audioStreamIndex, new LiveStreamRequest
DeviceProfile = deviceProfile?.DeviceProfile, {
EnableDirectPlay = enableDirectPlay, AudioStreamIndex = audioStreamIndex,
EnableDirectStream = enableDirectStream, DeviceProfile = deviceProfile?.DeviceProfile,
ItemId = itemId, EnableDirectPlay = enableDirectPlay,
MaxAudioChannels = maxAudioChannels, EnableDirectStream = enableDirectStream,
MaxStreamingBitrate = maxStreamingBitrate, ItemId = itemId,
PlaySessionId = info.PlaySessionId, MaxAudioChannels = maxAudioChannels,
StartTimeTicks = startTimeTicks, MaxStreamingBitrate = maxStreamingBitrate,
SubtitleStreamIndex = subtitleStreamIndex, PlaySessionId = info.PlaySessionId,
UserId = userId ?? Guid.Empty, StartTimeTicks = startTimeTicks,
OpenToken = mediaSource.OpenToken SubtitleStreamIndex = subtitleStreamIndex,
}).ConfigureAwait(false); UserId = userId ?? Guid.Empty,
OpenToken = mediaSource.OpenToken
}).ConfigureAwait(false);
info.MediaSources = new[] { openStreamResult.MediaSource }; info.MediaSources = new[] { openStreamResult.MediaSource };
} }
@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers
{ {
foreach (var mediaSource in info.MediaSources) 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, EnableDirectStream = enableDirectStream,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
}; };
return await OpenMediaSource(request).ConfigureAwait(false); return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers
ArrayPool<byte>.Shared.Return(buffer); 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();
}
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,6 @@ using System.Collections.Generic;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Dto namespace MediaBrowser.Controller.Dto
{ {
@ -11,20 +10,6 @@ namespace MediaBrowser.Controller.Dto
/// </summary> /// </summary>
public interface IDtoService 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> /// <summary>
/// Gets the primary image aspect ratio. /// Gets the primary image aspect ratio.
/// </summary> /// </summary>
@ -32,15 +17,6 @@ namespace MediaBrowser.Controller.Dto
/// <returns>System.Nullable&lt;System.Double&gt;.</returns> /// <returns>System.Nullable&lt;System.Double&gt;.</returns>
double? GetPrimaryImageAspectRatio(BaseItem item); 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> /// <summary>
/// Gets the base item dto. /// Gets the base item dto.
/// </summary> /// </summary>

View file

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

View file

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

View file

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

View file

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

View file

@ -465,9 +465,16 @@ namespace MediaBrowser.Providers.Manager
try 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; result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
return true; return true;
@ -565,14 +572,14 @@ namespace MediaBrowser.Providers.Manager
try 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 there's already an image of the same size, skip it
if (response.ContentLength.HasValue) if (response.Content.Headers.ContentLength.HasValue)
{ {
try 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(); response.Content.Dispose();
continue; 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; result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
} }
catch (HttpException ex) catch (HttpException ex)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,12 +8,12 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Music
internal static MusicBrainzAlbumProvider Current; internal static MusicBrainzAlbumProvider Current;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly ILogger<MusicBrainzAlbumProvider> _logger; private readonly ILogger<MusicBrainzAlbumProvider> _logger;
@ -51,11 +51,11 @@ namespace MediaBrowser.Providers.Music
private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
public MusicBrainzAlbumProvider( public MusicBrainzAlbumProvider(
IHttpClient httpClient, IHttpClientFactory httpClientFactory,
IApplicationHost appHost, IApplicationHost appHost,
ILogger<MusicBrainzAlbumProvider> logger) ILogger<MusicBrainzAlbumProvider> logger)
{ {
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
_logger = logger; _logger = logger;
@ -123,11 +123,9 @@ namespace MediaBrowser.Providers.Music
if (!string.IsNullOrWhiteSpace(url)) if (!string.IsNullOrWhiteSpace(url))
{ {
using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
{ return GetResultsFromResponse(stream);
return GetResultsFromResponse(stream);
}
} }
return Enumerable.Empty<RemoteSearchResult>(); return Enumerable.Empty<RemoteSearchResult>();
@ -282,23 +280,19 @@ namespace MediaBrowser.Providers.Music
WebUtility.UrlEncode(albumName), WebUtility.UrlEncode(albumName),
artistId); artistId);
using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var oReader = new StreamReader(stream, Encoding.UTF8)) using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings
{ {
var settings = new XmlReaderSettings() ValidationType = ValidationType.None,
{ CheckCharacters = false,
ValidationType = ValidationType.None, IgnoreProcessingInstructions = true,
CheckCharacters = false, IgnoreComments = true
IgnoreProcessingInstructions = true, };
IgnoreComments = true
};
using (var reader = XmlReader.Create(oReader, settings)) using var reader = XmlReader.Create(oReader, settings);
{ return ReleaseResult.Parse(reader).FirstOrDefault();
return ReleaseResult.Parse(reader).FirstOrDefault();
}
}
} }
private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken) private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
@ -309,23 +303,19 @@ namespace MediaBrowser.Providers.Music
WebUtility.UrlEncode(albumName), WebUtility.UrlEncode(albumName),
WebUtility.UrlEncode(artistName)); WebUtility.UrlEncode(artistName));
using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var oReader = new StreamReader(stream, Encoding.UTF8)) using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings()
{ {
var settings = new XmlReaderSettings() ValidationType = ValidationType.None,
{ CheckCharacters = false,
ValidationType = ValidationType.None, IgnoreProcessingInstructions = true,
CheckCharacters = false, IgnoreComments = true
IgnoreProcessingInstructions = true, };
IgnoreComments = true
};
using (var reader = XmlReader.Create(oReader, settings)) using var reader = XmlReader.Create(oReader, settings);
{ return ReleaseResult.Parse(reader).FirstOrDefault();
return ReleaseResult.Parse(reader).FirstOrDefault();
}
}
} }
private class ReleaseResult private class ReleaseResult
@ -624,30 +614,21 @@ namespace MediaBrowser.Providers.Music
{ {
var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture);
using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var oReader = new StreamReader(stream, Encoding.UTF8)) using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings
{ {
var settings = new XmlReaderSettings() ValidationType = ValidationType.None,
{ CheckCharacters = false,
ValidationType = ValidationType.None, IgnoreProcessingInstructions = true,
CheckCharacters = false, IgnoreComments = true
IgnoreProcessingInstructions = true, };
IgnoreComments = true
};
using (var reader = XmlReader.Create(oReader, settings)) using var reader = XmlReader.Create(oReader, settings);
{ var result = ReleaseResult.Parse(reader).FirstOrDefault();
var result = ReleaseResult.Parse(reader).FirstOrDefault();
if (result != null) return result?.ReleaseId;
{
return result.ReleaseId;
}
}
}
return null;
} }
/// <summary> /// <summary>
@ -660,59 +641,57 @@ namespace MediaBrowser.Providers.Music
{ {
var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture); var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture);
using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
using (var stream = response.Content) await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var oReader = new StreamReader(stream, Encoding.UTF8)) using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings
{ {
var settings = new XmlReaderSettings() ValidationType = ValidationType.None,
{ CheckCharacters = false,
ValidationType = ValidationType.None, IgnoreProcessingInstructions = true,
CheckCharacters = false, IgnoreComments = true
IgnoreProcessingInstructions = true, };
IgnoreComments = true
};
using (var reader = XmlReader.Create(oReader, settings)) using (var reader = XmlReader.Create(oReader, settings))
{ {
reader.MoveToContent(); reader.MoveToContent();
reader.Read(); reader.Read();
// Loop through each element // Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive) 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)
{ {
if (reader.IsEmptyElement) reader.Read();
{ continue;
reader.Read(); }
continue;
}
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
return GetFirstReleaseGroupId(subReader); return GetFirstReleaseGroupId(subReader);
} }
} }
default: default:
{ {
reader.Skip(); reader.Skip();
break; break;
}
} }
} }
else
{
reader.Read();
}
} }
else
return null; {
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 /// A number of retries shall be made in order to try and satisfy the request before
/// giving up and returning null. /// giving up and returning null.
/// </summary> /// </summary>
internal async Task<HttpResponseInfo> GetMusicBrainzResponse(string url, CancellationToken cancellationToken) internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
{ {
var options = new HttpRequestOptions using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url);
{
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
};
HttpResponseInfo response; // 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)));
HttpResponseMessage response;
var attempts = 0u; var attempts = 0u;
do do
@ -790,7 +765,7 @@ namespace MediaBrowser.Providers.Music
_logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
_stopWatchMusicBrainz.Restart(); _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) // 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 // Log error if unable to query MB database due to throttling
if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) 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; return response;
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View file

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

View file

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

View file

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

View file

@ -5,10 +5,10 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder
{ {
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
@ -35,13 +35,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public OmdbItemProvider( public OmdbItemProvider(
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IApplicationHost appHost, IApplicationHost appHost,
IHttpClient httpClient, IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IFileSystem fileSystem, IFileSystem fileSystem,
IServerConfigurationManager configurationManager) IServerConfigurationManager configurationManager)
{ {
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_configurationManager = configurationManager; _configurationManager = configurationManager;
@ -129,67 +129,63 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var url = OmdbProvider.GetOmdbUrl(urlQuery, _appHost, cancellationToken); 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>(); resultList.AddRange(searchResultList.Search);
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;
});
} }
} }
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) 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.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true; 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; return result;
@ -256,7 +252,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId); result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true; 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; return result;
@ -276,13 +272,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return first == null ? null : first.GetProviderId(MetadataProvider.Imdb); 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 return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
class SearchResult class SearchResult

View file

@ -10,7 +10,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -25,14 +24,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IApplicationHost _appHost; 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; _jsonSerializer = jsonSerializer;
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_appHost = appHost; _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); 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 response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
{ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var stream = response.Content) var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
{ Directory.CreateDirectory(Path.GetDirectoryName(path));
var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false); _jsonSerializer.SerializeToFile(rootObject, path);
Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(rootObject, path);
}
}
return 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); 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 response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false);
{ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var stream = response.Content) var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
{ Directory.CreateDirectory(Path.GetDirectoryName(path));
var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false); _jsonSerializer.SerializeToFile(rootObject, path);
Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(rootObject, path);
}
}
return 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 return httpClient.GetAsync(url, cancellationToken);
{
Url = url,
CancellationToken = cancellationToken,
BufferContent = true,
EnableDefaultUserAgent = true
}, HttpMethod.Get);
} }
internal string GetDataFilePath(string imdbId) internal string GetDataFilePath(string imdbId)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -20,11 +20,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{ {
public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder 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; public string Name => ProviderName;
@ -153,13 +153,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public int Order => 0; 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 return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
} }
} }

View file

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

View file

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

View file

@ -6,11 +6,11 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
@ -18,7 +18,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies; using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
@ -34,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
internal static TmdbMovieProvider Current { get; private set; } internal static TmdbMovieProvider Current { get; private set; }
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<TmdbMovieProvider> _logger; private readonly ILogger<TmdbMovieProvider> _logger;
@ -45,7 +44,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public TmdbMovieProvider( public TmdbMovieProvider(
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IHttpClient httpClient, IHttpClientFactory httpClientFactory,
IFileSystem fileSystem, IFileSystem fileSystem,
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
ILogger<TmdbMovieProvider> logger, ILogger<TmdbMovieProvider> logger,
@ -53,7 +52,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
IApplicationHost appHost) IApplicationHost appHost)
{ {
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_logger = logger; _logger = logger;
@ -146,20 +145,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return _tmdbSettings; 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), requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
using (Stream json = response.Content)
{
_tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(json).ConfigureAwait(false);
return _tmdbSettings;
}
} }
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}"; 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); url += "&include_image_language=" + GetImageLanguagesParam(language);
} }
MovieResult mainResult;
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Cache if not using a tmdbId because we won't have the tmdb cache directory structure. So use the lower level cache. using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
var cacheMode = isTmdbId ? CacheMode.None : CacheMode.Unconditional; foreach (var header in TmdbUtils.AcceptHeaders)
var cacheLength = TimeSpan.FromDays(3);
try
{ {
using (var response = await GetMovieDbResponse(new HttpRequestOptions requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
{
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);
}
}
} }
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(); cancellationToken.ThrowIfCancellationRequested();
// If the language preference isn't english, then have the overview fallback to english if it's blank // 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); url += "&include_image_language=" + GetImageLanguagesParam(language);
} }
using (var response = await GetMovieDbResponse(new HttpRequestOptions using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
foreach (var header in TmdbUtils.AcceptHeaders)
{ {
Url = url, langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader,
CacheMode = cacheMode,
CacheLength = cacheLength
}).ConfigureAwait(false))
{
using (var json = response.Content)
{
var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
mainResult.Overview = englishResult.Overview;
}
} }
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; return mainResult;
@ -409,25 +380,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// <summary> /// <summary>
/// Gets the movie db response. /// Gets the movie db response.
/// </summary> /// </summary>
internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options) internal async Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message)
{ {
options.BufferContent = true; message.Headers.UserAgent.Add(new ProductInfoHeaderValue(_appHost.ApplicationUserAgent));
options.UserAgent = _appHost.ApplicationUserAgent; return await _httpClientFactory.CreateClient().SendAsync(message);
return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
public int Order => 1; public int Order => 1;
/// <inheritdoc /> /// <inheritdoc />
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{ {
return _httpClient.GetResponse(new HttpRequestOptions return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
} }
} }

View file

@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; 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); 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, requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
using (var json = response.Content)
{
var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(json).ConfigureAwait(false);
var results = searchResults.Results ?? new List<MovieResult>();
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))
{
remoteResult.PremiereDate = r.ToUniversalTime();
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
}
}
remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
return remoteResult;
})
.ToList();
}
} }
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>();
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))
{
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) 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"); var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv");
using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
foreach (var header in TmdbUtils.AcceptHeaders)
{ {
Url = url3, requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
using (var json = response.Content)
{
var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(json).ConfigureAwait(false);
var results = searchResults.Results ?? new List<TvResult>();
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))
{
remoteResult.PremiereDate = r.ToUniversalTime();
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
}
}
remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
return remoteResult;
})
.ToList();
}
} }
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>();
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))
{
remoteResult.PremiereDate = r.ToUniversalTime();
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
}
}
remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
return remoteResult;
})
.ToList();
} }
} }
} }

View file

@ -2,9 +2,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
public string Name => TmdbMovieProvider.Current.Name; 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(); throw new NotImplementedException();
} }

View file

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

View file

@ -6,11 +6,12 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -36,20 +37,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClient _httpClient; private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TmdbPersonProvider> _logger; private readonly ILogger<TmdbPersonProvider> _logger;
public TmdbPersonProvider( public TmdbPersonProvider(
IFileSystem fileSystem, IFileSystem fileSystem,
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IHttpClient httpClient, IHttpClientFactory httpClientFactory,
ILogger<TmdbPersonProvider> logger) ILogger<TmdbPersonProvider> logger)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_httpClient = httpClient; _httpClientFactory = httpClientFactory;
_logger = logger; _logger = logger;
Current = this; 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); 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, requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
using (var json = response.Content)
{
var result = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(json).ConfigureAwait(false) ??
new TmdbSearchResult<PersonSearchResult>();
return result.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
}
} }
var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
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) 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); 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 using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
foreach (var header in TmdbUtils.AcceptHeaders)
{ {
Url = url, requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
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);
}
}
} }
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) 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"); 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 return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
} }
} }

View file

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

View file

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

View file

@ -3,9 +3,10 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -19,16 +20,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public abstract class TmdbEpisodeProviderBase 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 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 IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ILogger<TmdbEpisodeProviderBase> _logger; 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; _configurationManager = configurationManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem; _fileSystem = fileSystem;
@ -124,27 +125,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
cancellationToken.ThrowIfCancellationRequested(); 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, requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
using (var json = response.Content)
{
return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(json).ConfigureAwait(false);
}
} }
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 return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
} }
} }

View file

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

View file

@ -5,9 +5,10 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -26,7 +27,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo> 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 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 IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
@ -35,9 +36,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
internal static TmdbSeasonProvider Current { get; private set; } 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; _configurationManager = configurationManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_localization = localization; _localization = localization;
@ -121,13 +122,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>()); 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 return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
@ -215,18 +212,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
cancellationToken.ThrowIfCancellationRequested(); 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, requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
CancellationToken = cancellationToken,
AcceptHeader = TmdbUtils.AcceptHeader
}).ConfigureAwait(false))
{
using (var json = response.Content)
{
return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(json).ConfigureAwait(false);
}
} }
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);
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
@ -13,11 +13,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
{ {
public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo> 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) public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
@ -34,13 +34,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
public int Order => 0; 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 return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
{
CancellationToken = cancellationToken,
Url = url
});
} }
} }
} }

View file

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