mirror of
https://github.com/jellyfin/jellyfin
synced 2024-10-15 04:02:32 +00:00
Merge branch 'master' into event-rewrite-1
This commit is contained in:
commit
98ed90c4a2
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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": "প্রিয় গানগুলো",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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} ஐ இயக்குகிறது"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
195
Jellyfin.Api/Helpers/AudioHelper.cs
Normal file
195
Jellyfin.Api/Helpers/AudioHelper.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
550
Jellyfin.Api/Helpers/DynamicHlsHelper.cs
Normal file
550
Jellyfin.Api/Helpers/DynamicHlsHelper.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
573
Jellyfin.Api/Helpers/MediaInfoHelper.cs
Normal file
573
Jellyfin.Api/Helpers/MediaInfoHelper.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
Normal file
44
Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
31
Jellyfin.Server/Formatters/XmlOutputFormatter.cs
Normal file
31
Jellyfin.Server/Formatters/XmlOutputFormatter.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<System.Double>.</returns>
|
/// <returns>System.Nullable<System.Double>.</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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
@ -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-->
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue