Merge branch 'master' into event-rewrite-1

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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