diff options
| author | BaronGreenback <jimcartlidge@yahoo.co.uk> | 2020-09-05 20:49:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-09-05 20:49:04 +0100 |
| commit | 8439860b2aa38f4f0e8fa243f8efaa9b5a0d0c0b (patch) | |
| tree | 577b5eb958178ccc71e74083cf9871c889ce09fe /Emby.Server.Implementations/LiveTv | |
| parent | e33824d28667df0344420d42032fbb01e9f8f659 (diff) | |
| parent | 9cffa66a015c5200e461f396cd408949591d9552 (diff) | |
Merge branch 'master' into Plugins
Diffstat (limited to 'Emby.Server.Implementations/LiveTv')
15 files changed, 409 insertions, 606 deletions
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e13a3bb3..44560d1e2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public class DirectRecorder : IRecorder { private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IStreamHelper _streamHelper; - public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper) + public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper) { _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _streamHelper = streamHelper; } @@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false); + await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false); } _logger.LogInformation("Recording completed to file {0}", targetFile); @@ -63,37 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - var httpRequestOptions = new HttpRequestOptions - { - Url = mediaSource.Path, - BufferContent = false, - - // Some remote urls will expect a user agent to be supplied - UserAgent = "Emby/3.0", + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - // Shouldn't matter but may cause issues - DecompressionMethod = CompressionMethods.None - }; + _logger.LogInformation("Opened recording stream from tuner provider"); - using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false)) - { - _logger.LogInformation("Opened recording stream from tuner provider"); + Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); + await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read); - using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - onStarted(); + onStarted(); - _logger.LogInformation("Copying recording stream to file {0}", targetFile); + _logger.LogInformation("Copying recording stream to file {0}", targetFile); - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + // The media source if infinite so we need to handle stopping ourselves + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false); - } - } + await _streamHelper.CopyUntilCancelled( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + output, + IODefaults.CopyToBufferSize, + cancellationToken).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 7b0fcbc9e..ca60c3366 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -7,11 +7,14 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using Emby.Server.Implementations.Library; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -28,7 +31,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; @@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IServerApplicationHost _appHost; private readonly ILogger<EmbyTV> _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; @@ -80,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IMediaSourceManager mediaSourceManager, ILogger<EmbyTV> logger, IJsonSerializer jsonSerializer, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, @@ -93,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _appHost = appHost; _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _config = config; _fileSystem = fileSystem; _libraryManager = libraryManager; @@ -603,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return Task.CompletedTask; } - public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -807,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings() - { - return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested); - } - public ActiveRecordingInfo GetActiveRecordingInfo(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -1014,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new Exception("Tuner not found."); } - private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) - { - var json = _jsonSerializer.SerializeToString(mediaSource); - mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); - - mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; - - return mediaSource; - } - public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelId)) @@ -1653,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); } - return new DirectRecorder(_logger, _httpClient, _streamHelper); + return new DirectRecorder(_logger, _httpClientFactory, _streamHelper); } private void OnSuccessfulRecording(TimerInfo timer, string path) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index d8ec107ec..3e5457dbd 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,12 +8,9 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); + private bool _hasExited; private Stream _logFileStream; private string _targetPath; private Process _process; - private readonly IJsonSerializer _json; - private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); - private readonly IServerConfigurationManager _config; public EncodedRecorder( ILogger logger, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, - IJsonSerializer json, - IServerConfigurationManager config) + IJsonSerializer json) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _config = config; } private static bool CopySubtitles => false; @@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } - private EncodingOptions GetEncodingOptions() - { - return _config.GetConfiguration<EncodingOptions>("encoding"); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; @@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV StartInfo = processStartInfo, EnableRaisingEvents = true }; - _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process); _process.Start(); @@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } protected string GetOutputSizeParam() - { - var filters = new List<string>(); - - filters.Add("yadif=0:-1:0"); - - var output = string.Empty; - - if (filters.Count > 0) - { - output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); - } - - return output; - } + => "-vf \"yadif=0:-1:0\""; private void Stop() { @@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// <summary> /// Processes the exited. /// </summary> - private void OnFfMpegProcessExited(Process process, string inputFile) + private void OnFfMpegProcessExited(Process process) { using (process) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index 69a9cb78a..a2ec2df37 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Plugins; namespace Emby.Server.Implementations.LiveTv.EmbyTV { - public class EntryPoint : IServerEntryPoint + public sealed class EntryPoint : IServerEntryPoint { /// <inheritdoc /> public Task RunAsync() diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 285a59a24..dd479b7d1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -5,8 +5,8 @@ using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Threading; +using Jellyfin.Data.Events; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Events; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 3709f8fe4..f9ae55af8 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -8,6 +8,8 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mime; +using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { + private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; + private readonly ILogger<SchedulesDirect> _logger; private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly IApplicationHost _appHost; - private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - public SchedulesDirect( ILogger<SchedulesDirect> logger, IJsonSerializer jsonSerializer, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IApplicationHost appHost) { _logger = logger; _jsonSerializer = jsonSerializer; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; } @@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings while (start <= end) { - dates.Add(start.ToString("yyyy-MM-dd")); + dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); start = start.AddDays(1); } @@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings var requestString = _jsonSerializer.SerializeToString(requestList); _logger.LogDebug("Request string for schedules is: {RequestString}", requestString); - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/schedules", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - RequestContent = requestString - }; + using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules"); + options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json); + options.Headers.TryAddWithoutValidation("token", token); + using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); + await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false); + _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); - httpOptions.RequestHeaders["token"] = token; + using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs"); + programRequestOptions.Headers.TryAddWithoutValidation("token", token); - using (var response = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false); - _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); + var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct(); + programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json); - httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/programs", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - httpOptions.RequestHeaders["token"] = token; + using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); + await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false); + var programDict = programDetails.ToDictionary(p => p.programID, y => y); - var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct(); - httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]"; + var programIdsWithImages = + programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID) + .ToList(); - using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false); - var programDict = programDetails.ToDictionary(p => p.programID, y => y); - - var programIdsWithImages = - programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID) - .ToList(); + var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); - var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); + var programsInfo = new List<ProgramInfo>(); + foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs)) + { + // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID + + // " which corresponds to channel " + channelNumber + " and program id " + + // schedule.programID + " which says it has images? " + + // programDict[schedule.programID].hasImageArtwork); - var programsInfo = new List<ProgramInfo>(); - foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs)) + if (images != null) + { + var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); + if (imageIndex > -1) { - // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID + - // " which corresponds to channel " + channelNumber + " and program id " + - // schedule.programID + " which says it has images? " + - // programDict[schedule.programID].hasImageArtwork); - - if (images != null) - { - var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); - if (imageIndex > -1) - { - var programEntry = programDict[schedule.programID]; + var programEntry = programDict[schedule.programID]; - var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>(); - var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); - var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); + var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>(); + var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); + var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); - const double DesiredAspect = 2.0 / 3; + const double DesiredAspect = 2.0 / 3; - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, DesiredAspect); + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, true, DesiredAspect); - const double WideAspect = 16.0 / 9; + const double WideAspect = 16.0 / 9; - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); + programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); - // Don't supply the same image twice - if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) - { - programEntry.thumbImage = null; - } - - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); - - // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LOT", false); - } + // Don't supply the same image twice + if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) + { + programEntry.thumbImage = null; } - programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); - } + programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); - return programsInfo; + // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LOT", false); + } } + + programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); } + + return programsInfo; } private static int GetSizeOrder(ScheduleDirect.ImageData image) @@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (!string.IsNullOrWhiteSpace(details.originalAirDate)) { - info.OriginalAirDate = DateTime.Parse(details.originalAirDate); + info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture); info.ProductionYear = info.OriginalAirDate.Value.Year; } if (details.movie != null) { - if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year)) + if (!string.IsNullOrEmpty(details.movie.year) + && int.TryParse(details.movie.year, out int year)) { info.ProductionYear = year; } @@ -461,7 +447,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( ListingsProviderInfo info, List<string> programIds, - CancellationToken cancellationToken) + CancellationToken cancellationToken) { if (programIds.Count == 0) { @@ -474,7 +460,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var imageId = i.Substring(0, 10); - if (!imageIdString.Contains(imageId)) + if (!imageIdString.Contains(imageId, StringComparison.Ordinal)) { imageIdString += "\"" + imageId + "\","; } @@ -482,22 +468,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings imageIdString = imageIdString.TrimEnd(',') + "]"; - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/metadata/programs", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - RequestContent = imageIdString, - LogErrorResponseBody = true, - }; + using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs"); + message.Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json); try { - using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>( - innerResponse2.Content).ConfigureAwait(false); - } + using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); + await using var response = await innerResponse2.Content.ReadAsStreamAsync(); + return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>( + response).ConfigureAwait(false); } catch (Exception ex) { @@ -518,41 +497,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings return lineups; } - var options = new HttpRequestOptions() - { - Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - options.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location); + options.Headers.TryAddWithoutValidation("token", token); try { - using (var httpResponse = await Get(options, false, info).ConfigureAwait(false)) - using (Stream responce = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false); + using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); + await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + + var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false); - if (root != null) + if (root != null) + { + foreach (ScheduleDirect.Headends headend in root) { - foreach (ScheduleDirect.Headends headend in root) + foreach (ScheduleDirect.Lineup lineup in headend.lineups) { - foreach (ScheduleDirect.Lineup lineup in headend.lineups) + lineups.Add(new NameIdPair { - lineups.Add(new NameIdPair - { - Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name, - Id = lineup.uri.Substring(18) - }); - } + Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name, + Id = lineup.uri.Substring(18) + }); } } - else - { - _logger.LogInformation("No lineups available"); - } + } + else + { + _logger.LogInformation("No lineups available"); } } catch (Exception ex) @@ -587,7 +558,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - NameValuePair savedToken = null; + NameValuePair savedToken; if (!_tokens.TryGetValue(username, out savedToken)) { savedToken = new NameValuePair(); @@ -633,46 +604,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - private async Task<HttpResponseInfo> Post(HttpRequestOptions options, - bool enableRetry, - ListingsProviderInfo providerInfo) - { - // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethods.Deflate; - - try - { - return await _httpClient.Post(options).ConfigureAwait(false); - } - catch (HttpException ex) - { - _tokens.Clear(); - - if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) - { - enableRetry = false; - } - - if (!enableRetry) - { - throw; - } - } - - options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); - return await Post(options, false, providerInfo).ConfigureAwait(false); - } - - private async Task<HttpResponseInfo> Get(HttpRequestOptions options, + private async Task<HttpResponseMessage> Send( + HttpRequestMessage options, bool enableRetry, - ListingsProviderInfo providerInfo) + ListingsProviderInfo providerInfo, + CancellationToken cancellationToken, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethods.Deflate; - try { - return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -689,35 +630,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); - return await Get(options, false, providerInfo).ConfigureAwait(false); + options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); + return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false); } - private async Task<string> GetTokenInternal(string username, string password, + private async Task<string> GetTokenInternal( + string username, + string password, CancellationToken cancellationToken) { - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/token", - UserAgent = UserAgent, - RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - // _logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " + - // httpOptions.RequestContent); + using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); + options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); - using (var response = await Post(httpOptions, false, null).ConfigureAwait(false)) + using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false); + if (root.message == "OK") { - var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(response.Content).ConfigureAwait(false); - if (root.message == "OK") - { - _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); - return root.token; - } - - throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); + _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); + return root.token; } + + throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); } private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -736,20 +670,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Adding new LineUp "); - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups/" + info.ListingsId, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - BufferContent = false - }; - - httpOptions.RequestHeaders["token"] = token; - - using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false)) - { - } + using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); + options.Headers.TryAddWithoutValidation("token", token); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -768,25 +691,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Headends on account "); - var options = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - options.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups"); + options.Headers.TryAddWithoutValidation("token", token); try { - using (var httpResponse = await Get(options, false, null).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false); + using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var response = httpResponse.Content; + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false); - return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase)); - } + return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase)); } catch (HttpException ex) { @@ -851,55 +766,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings throw new Exception("token required"); } - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups/" + listingsId, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - }; - - httpOptions.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); + options.Headers.TryAddWithoutValidation("token", token); var list = new List<ChannelInfo>(); - using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false); - _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count); - _logger.LogInformation("Mapping Stations to Channel"); - - var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>(); + using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); + await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false); + _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count); + _logger.LogInformation("Mapping Stations to Channel"); - foreach (ScheduleDirect.Map map in root.map) - { - var channelNumber = GetChannelNumber(map); + var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>(); - var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase)); - if (station == null) - { - station = new ScheduleDirect.Station - { - stationID = map.stationID - }; - } + foreach (ScheduleDirect.Map map in root.map) + { + var channelNumber = GetChannelNumber(map); - var channelInfo = new ChannelInfo - { - Id = station.stationID, - CallSign = station.callsign, - Number = channelNumber, - Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name - }; + var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase)); + if (station == null) + { + station = new ScheduleDirect.Station { stationID = map.stationID }; + } - if (station.logo != null) - { - channelInfo.ImageUrl = station.logo.URL; - } + var channelInfo = new ChannelInfo + { + Id = station.stationID, + CallSign = station.callsign, + Number = channelNumber, + Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name + }; - list.Add(channelInfo); + if (station.logo != null) + { + channelInfo.ImageUrl = station.logo.URL; } + + list.Add(channelInfo); } return list; @@ -929,7 +832,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static string NormalizeName(string value) { - return value.Replace(" ", string.Empty).Replace("-", string.Empty); + return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal); } public class ScheduleDirect diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 0a93c4674..2d6f453bd 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings public class XmlTvListingsProvider : IListingsProvider { private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger<XmlTvListingsProvider> _logger; private readonly IFileSystem _fileSystem; private readonly IZipClient _zipClient; public XmlTvListingsProvider( IServerConfigurationManager config, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILogger<XmlTvListingsProvider> logger, IFileSystem fileSystem, IZipClient zipClient) { _config = config; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _fileSystem = fileSystem; _zipClient = zipClient; @@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); - using (var res = await _httpClient.SendAsync( - new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = path, - DecompressionMethod = CompressionMethods.Gzip, - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var stream = res.Content) - using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) { - if (res.ContentHeaders.ContentEncoding.Contains("gzip")) - { - using (var gzStream = new GZipStream(stream, CompressionMode.Decompress)) - { - await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } - } - else - { - await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } + await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } return UnzipIfNeeded(path, cacheFile); @@ -237,7 +220,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings && !programInfo.IsRepeat && (programInfo.EpisodeNumber ?? 0) == 0) { - programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); + programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); } } else @@ -246,7 +229,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } // Construct an id from the channel and start date - programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate); + programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate); if (programInfo.IsMovie) { @@ -296,7 +279,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings Name = c.DisplayName, ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number - }).ToList(); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 4c1de3bcc..a898a564f 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; @@ -23,12 +25,10 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; @@ -41,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv /// </summary> public class LiveTvManager : ILiveTvManager, IDisposable { + private const int MaxGuideDays = 14; private const string ExternalServiceTag = "ExternalServiceId"; private const string EtagKey = "ProgramEtag"; @@ -54,7 +55,6 @@ namespace Emby.Server.Implementations.LiveTv private readonly ILibraryManager _libraryManager; private readonly ITaskManager _taskManager; private readonly ILocalizationManager _localization; - private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IChannelManager _channelManager; private readonly LiveTvDtoService _tvDtoService; @@ -73,7 +73,6 @@ namespace Emby.Server.Implementations.LiveTv ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, - IJsonSerializer jsonSerializer, IFileSystem fileSystem, IChannelManager channelManager, LiveTvDtoService liveTvDtoService) @@ -85,7 +84,6 @@ namespace Emby.Server.Implementations.LiveTv _libraryManager = libraryManager; _taskManager = taskManager; _localization = localization; - _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; _dtoService = dtoService; _userDataManager = userDataManager; @@ -424,7 +422,7 @@ namespace Emby.Server.Implementations.LiveTv } } - private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) + private async Task<LiveTvChannel> GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; var isNew = false; @@ -514,7 +512,7 @@ namespace Emby.Server.Implementations.LiveTv } else if (forceUpdate) { - _libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken); + await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } return item; @@ -563,7 +561,7 @@ namespace Emby.Server.Implementations.LiveTv item.Audio = info.Audio; item.ChannelId = channel.Id; - item.CommunityRating = item.CommunityRating ?? info.CommunityRating; + item.CommunityRating ??= info.CommunityRating; if ((item.CommunityRating ?? 0).Equals(0)) { item.CommunityRating = null; @@ -648,8 +646,8 @@ namespace Emby.Server.Implementations.LiveTv item.IsSeries = isSeries; item.Name = info.Name; - item.OfficialRating = item.OfficialRating ?? info.OfficialRating; - item.Overview = item.Overview ?? info.Overview; + item.OfficialRating ??= info.OfficialRating; + item.Overview ??= info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; item.ProviderIds = info.ProviderIds; @@ -686,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.ImagePath)) { - item.SetImage(new ItemImageInfo - { - Path = info.ImagePath, - Type = ImageType.Primary - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ImagePath, + Type = ImageType.Primary + }, + 0); } else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.ImageUrl, - Type = ImageType.Primary - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ImageUrl, + Type = ImageType.Primary + }, + 0); } } @@ -706,11 +708,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.ThumbImageUrl, - Type = ImageType.Thumb - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ThumbImageUrl, + Type = ImageType.Thumb + }, + 0); } } @@ -718,11 +722,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.LogoImageUrl, - Type = ImageType.Logo - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.LogoImageUrl, + Type = ImageType.Logo + }, + 0); } } @@ -730,11 +736,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.BackdropImageUrl, - Type = ImageType.Backdrop - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.BackdropImageUrl, + Type = ImageType.Backdrop + }, + 0); } } @@ -789,7 +797,6 @@ namespace Emby.Server.Implementations.LiveTv if (query.OrderBy.Count == 0) { - // Unless something else was specified, order by start date to take advantage of a specialized index query.OrderBy = new[] { @@ -827,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) { - var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false); + var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer != null) { @@ -850,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); - var result = new QueryResult<BaseItemDto> + return new QueryResult<BaseItemDto> { Items = returnArray, TotalRecordCount = queryResult.TotalRecordCount }; - - return result; } public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) @@ -1124,7 +1129,7 @@ namespace Emby.Server.Implementations.LiveTv try { - var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken); + var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false); list.Add(item); } @@ -1141,7 +1146,7 @@ namespace Emby.Server.Implementations.LiveTv double percent = numComplete; percent /= allChannelsList.Count; - progress.Report(5 * percent + 10); + progress.Report((5 * percent) + 10); } progress.Report(15); @@ -1176,7 +1181,6 @@ namespace Emby.Server.Implementations.LiveTv var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, ChannelIds = new Guid[] { currentChannel.Id }, DtoOptions = new DtoOptions(true) @@ -1217,7 +1221,11 @@ namespace Emby.Server.Implementations.LiveTv if (updatedPrograms.Count > 0) { - _libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken); + await _libraryManager.UpdateItemsAsync( + updatedPrograms, + currentChannel, + ItemUpdateType.MetadataImport, + cancellationToken).ConfigureAwait(false); } currentChannel.IsMovie = isMovie; @@ -1230,7 +1238,7 @@ namespace Emby.Server.Implementations.LiveTv currentChannel.AddTag("Kids"); } - currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); await currentChannel.RefreshMetadata( new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -1301,8 +1309,6 @@ namespace Emby.Server.Implementations.LiveTv } } - private const int MaxGuideDays = 14; - private double GetGuideDays() { var config = GetConfiguration(); @@ -1715,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv if (timer == null) { - throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); + throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id)); } var service = GetService(timer.ServiceName); @@ -1734,7 +1740,7 @@ namespace Emby.Server.Implementations.LiveTv if (timer == null) { - throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id)); + throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id)); } var service = GetService(timer.ServiceName); @@ -1746,10 +1752,12 @@ namespace Emby.Server.Implementations.LiveTv public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) { - var results = await GetTimers(new TimerQuery - { - Id = id - }, cancellationToken).ConfigureAwait(false); + var results = await GetTimers( + new TimerQuery + { + Id = id + }, + cancellationToken).ConfigureAwait(false); return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); } @@ -1797,10 +1805,7 @@ namespace Emby.Server.Implementations.LiveTv } var returnArray = timers - .Select(i => - { - return i.Item1; - }) + .Select(i => i.Item1) .ToArray(); return new QueryResult<SeriesTimerInfo> @@ -1971,7 +1976,7 @@ namespace Emby.Server.Implementations.LiveTv if (service == null) { - service = _services.First(); + service = _services[0]; } var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); @@ -1997,9 +2002,7 @@ namespace Emby.Server.Implementations.LiveTv { var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false); - var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); - - return obj; + return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); } public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) @@ -2128,6 +2131,7 @@ namespace Emby.Server.Implementations.LiveTv public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } private bool _disposed = false; @@ -2234,7 +2238,7 @@ namespace Emby.Server.Implementations.LiveTv public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) { - info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info)); + info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info)); var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); @@ -2278,7 +2282,7 @@ namespace Emby.Server.Implementations.LiveTv { // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider - info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info)); + info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info)); var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); @@ -2450,8 +2454,7 @@ namespace Emby.Server.Implementations.LiveTv .SelectMany(i => i.Locations) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i != null) - .Where(i => i.IsVisibleStandalone(user)) + .Where(i => i != null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) .GroupBy(x => x.Id) .Select(x => x.First()) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index f3fc41352..8a0c0043a 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv public class LiveTvMediaSourceProvider : IMediaSourceProvider { // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. - private const char StreamIdDelimeter = '_'; - private const string StreamIdDelimeterString = "_"; + private const char StreamIdDelimiter = '_'; private readonly ILiveTvManager _liveTvManager; private readonly ILogger<LiveTvMediaSourceProvider> _logger; @@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv } } - return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>()); + return Task.FromResult(Enumerable.Empty<MediaSourceInfo>()); } private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) @@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv source.Id ?? string.Empty }; - source.OpenToken = string.Join(StreamIdDelimeterString, openKeys); + source.OpenToken = string.Join(StreamIdDelimiter, openKeys); } // Dummy this up so that direct play checks can still run @@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv /// <inheritdoc /> public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - var keys = openToken.Split(new[] { StreamIdDelimeter }, 3); + var keys = openToken.Split(StreamIdDelimiter, 3); var mediaSourceId = keys.Length >= 3 ? keys[2] : null; var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index a8d34d19c..fbcd4ef37 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -1,10 +1,10 @@ #pragma warning disable CS1591 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts @@ -23,17 +23,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { protected readonly IServerConfigurationManager Config; protected readonly ILogger<BaseTunerHost> Logger; - protected IJsonSerializer JsonSerializer; protected readonly IFileSystem FileSystem; - private readonly ConcurrentDictionary<string, ChannelCache> _channelCache = - new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase); + private readonly IMemoryCache _memoryCache; - protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem) + protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache) { Config = config; Logger = logger; - JsonSerializer = jsonSerializer; + _memoryCache = memoryCache; FileSystem = fileSystem; } @@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken) { - ChannelCache cache = null; var key = tuner.Id; - if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache)) + if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache)) { - return cache.Channels.ToList(); + return cache; } - var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); - var list = result.ToList(); + var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); if (!string.IsNullOrEmpty(key) && list.Count > 0) { - cache = cache ?? new ChannelCache(); - cache.Channels = list; - _channelCache.AddOrUpdate(key, cache, (k, v) => cache); + _memoryCache.Set(key, list); } return list; @@ -95,7 +89,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts try { Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); - JsonSerializer.SerializeToFile(channels, channelCacheFile); + await using var writeStream = File.OpenWrite(channelCacheFile); + await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (IOException) { @@ -110,7 +105,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { try { - var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile); + await using var readStream = File.OpenRead(channelCacheFile); + var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken) + .ConfigureAwait(false); list.AddRange(channels); } catch (IOException) @@ -233,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { return Config.GetConfiguration<LiveTvOptions>("livetv"); } - - private class ChannelCache - { - public List<ChannelInfo> Channels; - } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 2e2488e6e..28e30fac8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -23,32 +24,34 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; private readonly IStreamHelper _streamHelper; + private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); + public HdHomerunHost( IServerConfigurationManager config, ILogger<HdHomerunHost> logger, - IJsonSerializer jsonSerializer, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, - IStreamHelper streamHelper) - : base(config, logger, jsonSerializer, fileSystem) + IStreamHelper streamHelper, + IMemoryCache memoryCache) + : base(config, logger, fileSystem, memoryCache) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; @@ -68,25 +71,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - var options = new HttpRequestOptions - { - Url = model.LineupURL, - CancellationToken = cancellationToken, - BufferContent = false - }; + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false) ?? new List<Channels>(); - using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false)) - using (var stream = response.Content) + if (info.ImportFavoritesOnly) { - var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - - if (info.ImportFavoritesOnly) - { - lineup = lineup.Where(i => i.Favorite).ToList(); - } - - return lineup.Where(i => !i.DRM).ToList(); + lineup = lineup.Where(i => i.Favorite).ToList(); } + + return lineup.Where(i => !i.DRM).ToList(); } private class HdHomerunChannelInfo : ChannelInfo @@ -114,7 +109,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }).Cast<ChannelInfo>().ToList(); } - private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) { var cacheKey = info.Id; @@ -132,35 +126,31 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { - using (var response = await _httpClient.SendAsync(new HttpRequestOptions() - { - Url = string.Format("{0}/discover.json", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, HttpMethod.Get).ConfigureAwait(false)) - using (var stream = response.Content) - { - var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); - if (!string.IsNullOrEmpty(cacheKey)) + if (!string.IsNullOrEmpty(cacheKey)) + { + lock (_modelCache) { - lock (_modelCache) - { - _modelCache[cacheKey] = discoverResponse; - } + _modelCache[cacheKey] = discoverResponse; } - - return discoverResponse; } + + return discoverResponse; } catch (HttpException ex) { - if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) + if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) { - var defaultValue = "HDHR"; + const string DefaultValue = "HDHR"; var response = new DiscoverResponse { - ModelNumber = defaultValue + ModelNumber = DefaultValue }; if (!string.IsNullOrEmpty(cacheKey)) { @@ -182,50 +172,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var response = await _httpClient.SendAsync(new HttpRequestOptions() - { - Url = string.Format("{0}/tuners.html", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, HttpMethod.Get).ConfigureAwait(false)) - using (var stream = response.Content) - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); + var tuners = new List<LiveTvTunerInfo>(); + while (!sr.EndOfStream) { - var tuners = new List<LiveTvTunerInfo>(); - while (!sr.EndOfStream) + string line = StripXML(sr.ReadLine()); + if (line.Contains("Channel", StringComparison.Ordinal)) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) + LiveTvTunerStatus status; + var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = line.Substring(0, index - 1); + var currentChannel = line.Substring(index + 7); + if (currentChannel != "none") { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") - { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } - - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); + status = LiveTvTunerStatus.LiveTv; + } + else + { + status = LiveTvTunerStatus.Available; } - } - return tuners; + tuners.Add(new LiveTvTunerInfo + { + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); + } } + + return tuners; } private static string StripXML(string source) { + if (string.IsNullOrEmpty(source)) + { + return string.Empty; + } + char[] buffer = new char[source.Length]; int bufferIndex = 0; bool inside = false; @@ -270,7 +260,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun for (int i = 0; i < model.TunerCount; ++i) { - var name = string.Format("Tuner {0}", i + 1); + var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1); var currentChannel = "none"; // @todo Get current channel and map back to Station Id var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; @@ -626,7 +616,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun info, streamId, FileSystem, - _httpClient, + _httpClientFactory, Logger, Config, _appHost, @@ -725,7 +715,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // Need a way to set the Receive timeout on the socket otherwise this might never timeout? try { - await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken); + await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); var receiveBuffer = new byte[8192]; while (!cancellationToken.IsCancellationRequested) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 57c5b7500..d4a88e299 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public class HdHomerunManager : IDisposable + public sealed class HdHomerunManager : IDisposable { public const int HdHomeRunPort = 65001; @@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun StopStreaming(socket).GetAwaiter().GetResult(); } } + + GC.SuppressFinalize(this); } public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) @@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } _activeTuner = i; - var lockKeyString = string.Format("{0:d}", lockKeyValue); + var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue); var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); @@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun continue; } - var commandList = commands.GetCommands(); - foreach (var command in commandList) + foreach (var command in commands.GetCommands()) { var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); @@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); + var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort); var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index ff42a9747..8107bc427 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -18,7 +19,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; private readonly IMediaSourceManager _mediaSourceManager; @@ -36,15 +37,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger<M3UTunerHost> logger, - IJsonSerializer jsonSerializer, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, INetworkManager networkManager, - IStreamHelper streamHelper) - : base(config, logger, jsonSerializer, fileSystem) + IStreamHelper streamHelper, + IMemoryCache memoryCache) + : base(config, logger, fileSystem, memoryCache) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _networkManager = networkManager; _mediaSourceManager = mediaSourceManager; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var channelIdPrefix = GetFullChannelIdPrefix(info); - return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); + return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); } public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } } @@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task Validate(TunerHostInfo info) { - using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) + using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) { } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index c798c0a85..f066a749e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public class M3uParser { private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; - public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost) + public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost) { _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; } @@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return _httpClient.Get(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - // Some data providers will require a user agent - UserAgent = _appHost.ApplicationUserAgent - }); + return _httpClientFactory.CreateClient(NamedClient.Default) + .GetStreamAsync(url); } return Task.FromResult((Stream)File.OpenRead(url)); @@ -158,15 +154,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) { var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null; + var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; string numberString = null; string attributeValue; - double doubleValue; if (attributes.TryGetValue("tvg-chno", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } @@ -176,36 +171,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { if (attributes.TryGetValue("tvg-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } else if (attributes.TryGetValue("channel-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } } } - if (String.IsNullOrWhiteSpace(numberString)) + if (string.IsNullOrWhiteSpace(numberString)) { // Using this as a fallback now as this leads to Problems with channels like "5 USA" // where 5 isnt ment to be the channel number // Check for channel number with the format from SatIp // #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz - if (!string.IsNullOrWhiteSpace(nameInExtInf)) + if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace()) { var numberIndex = nameInExtInf.IndexOf(' '); if (numberIndex > 0) { - var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); + var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { - numberString = numberPart; + numberString = numberPart.ToString(); } } } @@ -231,7 +226,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { try { - numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last()); + numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]); if (!IsValidChannelNumber(numberString)) { @@ -258,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return false; } - if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { return false; } @@ -281,7 +276,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index bc4dcd894..6c10fca8c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class SharedHttpStream : LiveStream, IDirectStreamProvider { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; public SharedHttpStream( @@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILogger logger, IConfigurationManager configurationManager, IServerApplicationHost appHost, IStreamHelper streamHelper) : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; OriginalStreamId = originalStreamId; EnableStreamSharing = true; @@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var typeName = GetType().Name; Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url); - var httpRequestOptions = new HttpRequestOptions - { - Url = url, - CancellationToken = CancellationToken.None, - BufferContent = false, - DecompressionMethod = CompressionMethods.None - }; - - foreach (var header in mediaSource.RequiredHttpHeaders) - { - httpRequestOptions.RequestHeaders[header.Key] = header.Value; - } - - var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); var extension = "ts"; var requiresRemux = false; - var contentType = response.ContentType ?? string.Empty; + var contentType = response.Content.Headers.ContentType.ToString(); if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1) { requiresRemux = true; @@ -132,24 +121,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => { try { Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); - using (response) - using (var stream = response.Content) - using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); - } + using var message = response; + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException ex) { |
