aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/LiveTv
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/LiveTv')
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs77
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs765
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs201
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs54
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs171
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs19
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs56
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs1014
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs34
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs46
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs40
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs36
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs70
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs46
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs36
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs34
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs58
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs28
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs41
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs156
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs90
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs40
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs66
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs47
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs106
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs67
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs563
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs58
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs (renamed from Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs)50
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs63
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs23
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs42
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs35
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs473
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs582
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs328
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs38
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs224
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs91
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs148
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs175
64 files changed, 3686 insertions, 3059 deletions
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index dd636e6cd..6937cc097 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -1,7 +1,11 @@
+#pragma warning disable CS1591
+
using System;
using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -13,15 +17,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class DirectRecorder : IRecorder
{
private readonly ILogger _logger;
- private readonly IHttpClient _httpClient;
- private readonly IFileSystem _fileSystem;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IStreamHelper _streamHelper;
- public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, IStreamHelper streamHelper)
+ public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper)
{
_logger = logger;
- _httpClient = httpClient;
- _fileSystem = fileSystem;
+ _httpClientFactory = httpClientFactory;
_streamHelper = streamHelper;
}
@@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return targetFile;
}
- public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+ public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
if (directStreamProvider != null)
{
@@ -42,57 +44,56 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
- Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+ Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
onStarted();
- _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+ _logger.LogInformation("Copying recording to file {FilePath}", 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;
-
- await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
+ using var durationToken = new CancellationTokenSource(duration);
+ using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
+ var linkedCancellationToken = cancellationTokenSource.Token;
+
+ await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
+ await _streamHelper.CopyToAsync(
+ fileStream,
+ output,
+ IODefaults.CopyToBufferSize,
+ 1000,
+ linkedCancellationToken).ConfigureAwait(false);
}
- _logger.LogInformation("Recording completed to file {0}", targetFile);
+ _logger.LogInformation("Recording completed: {FilePath}", targetFile);
}
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
- var httpRequestOptions = new HttpRequestOptions
- {
- Url = mediaSource.Path,
- BufferContent = false,
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- // Some remote urls will expect a user agent to be supplied
- UserAgent = "Emby/3.0",
+ _logger.LogInformation("Opened recording stream from tuner provider");
- // Shouldn't matter but may cause issues
- EnableHttpCompression = false
- };
+ Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
- {
- _logger.LogInformation("Opened recording stream from tuner provider");
-
- Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+ await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
- using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.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
+ using var durationToken = new CancellationTokenSource(duration);
+ using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
+ cancellationToken = linkedCancellationToken.Token;
- await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false);
- }
- }
+ await _streamHelper.CopyUntilCancelled(
+ await response.Content.ReadAsStreamAsync(cancellationToken).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 fceb82ba1..980b42729 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1,17 +1,24 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+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;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -23,29 +30,27 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Reflection;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+
+ private const int TunerDiscoveryDurationMs = 3000;
+
private readonly IServerApplicationHost _appHost;
- private readonly ILogger _logger;
- private readonly IHttpClient _httpClient;
+ private readonly ILogger<EmbyTV> _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
- private readonly IJsonSerializer _jsonSerializer;
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
@@ -57,89 +62,104 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IProcessFactory _processFactory;
- private readonly IAssemblyInfo _assemblyInfo;
- private IMediaSourceManager _mediaSourceManager;
-
- public static EmbyTV Current;
-
- public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
- public event EventHandler<GenericEventArgs<string>> TimerCancelled;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IStreamHelper _streamHelper;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
- private readonly IStreamHelper _streamHelper;
+ private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
+ new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
+
+ private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
+
+ private bool _disposed = false;
- public EmbyTV(IServerApplicationHost appHost,
+ public EmbyTV(
+ IServerApplicationHost appHost,
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
- IAssemblyInfo assemblyInfo,
- ILogger logger,
- IJsonSerializer jsonSerializer,
- IHttpClient httpClient,
+ ILogger<EmbyTV> logger,
+ IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
ILiveTvManager liveTvManager,
IFileSystem fileSystem,
ILibraryManager libraryManager,
ILibraryMonitor libraryMonitor,
IProviderManager providerManager,
- IMediaEncoder mediaEncoder,
- IProcessFactory processFactory)
+ IMediaEncoder mediaEncoder)
{
Current = this;
_appHost = appHost;
_logger = logger;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_config = config;
_fileSystem = fileSystem;
_libraryManager = libraryManager;
_libraryMonitor = libraryMonitor;
_providerManager = providerManager;
_mediaEncoder = mediaEncoder;
- _processFactory = processFactory;
_liveTvManager = (LiveTvManager)liveTvManager;
- _jsonSerializer = jsonSerializer;
- _assemblyInfo = assemblyInfo;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
- _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
- _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
- _timerProvider.TimerFired += _timerProvider_TimerFired;
+ _seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
+ _timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
+ _timerProvider.TimerFired += OnTimerProviderTimerFired;
- _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+ _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
- private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated;
+
+ public event EventHandler<GenericEventArgs<string>> TimerCancelled;
+
+ public static EmbyTV Current { get; private set; }
+
+ /// <inheritdoc />
+ public string Name => "Emby";
+
+ public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv");
+
+ /// <inheritdoc />
+ public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
+
+ private string DefaultRecordingPath => Path.Combine(DataPath, "recordings");
+
+ private string RecordingPath
{
- if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
+ get
{
- OnRecordingFoldersChanged();
+ var path = GetConfiguration().RecordingPath;
+
+ return string.IsNullOrWhiteSpace(path)
+ ? DefaultRecordingPath
+ : path;
}
}
- public async Task Start()
+ private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
- _timerProvider.RestartTimers();
-
- await CreateRecordingFolders().ConfigureAwait(false);
+ if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
+ {
+ await CreateRecordingFolders().ConfigureAwait(false);
+ }
}
- private async void OnRecordingFoldersChanged()
+ public Task Start()
{
- await CreateRecordingFolders().ConfigureAwait(false);
+ _timerProvider.RestartTimers();
+
+ return CreateRecordingFolders();
}
internal async Task CreateRecordingFolders()
{
try
{
- var recordingFolders = GetRecordingFolders();
-
- var virtualFolders = _libraryManager.GetVirtualFolders()
- .ToList();
+ var recordingFolders = GetRecordingFolders().ToArray();
+ var virtualFolders = _libraryManager.GetVirtualFolders();
var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
@@ -156,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
continue;
}
- var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
+ var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
var libraryOptions = new LibraryOptions
{
@@ -189,7 +209,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
foreach (var path in pathsToRemove)
{
- await RemovePathFromLibrary(path).ConfigureAwait(false);
+ await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -198,13 +218,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private async Task RemovePathFromLibrary(string path)
+ private async Task RemovePathFromLibraryAsync(string path)
{
_logger.LogDebug("Removing path from library: {0}", path);
var requiresRefresh = false;
- var virtualFolders = _libraryManager.GetVirtualFolders()
- .ToList();
+ var virtualFolders = _libraryManager.GetVirtualFolders();
foreach (var virtualFolder in virtualFolders)
{
@@ -241,31 +260,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (requiresRefresh)
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
- }
- }
-
- public string Name => "Emby";
-
- public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv");
-
- private string DefaultRecordingPath => Path.Combine(DataPath, "recordings");
-
- private string RecordingPath
- {
- get
- {
- var path = GetConfiguration().RecordingPath;
-
- return string.IsNullOrWhiteSpace(path)
- ? DefaultRecordingPath
- : path;
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
}
- public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
-
- public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
+ public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
{
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
@@ -275,7 +274,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public async Task RefreshTimers(CancellationToken cancellationToken, IProgress<double> progress)
+ public async Task RefreshTimers(CancellationToken cancellationToken)
{
var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
@@ -343,7 +342,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (NotSupportedException)
{
-
}
catch (Exception ex)
{
@@ -355,7 +353,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return list;
}
- private async Task AddMetadata(IListingsProvider provider, ListingsProviderInfo info, List<ChannelInfo> tunerChannels, bool enableCache, CancellationToken cancellationToken)
+ private async Task AddMetadata(
+ IListingsProvider provider,
+ ListingsProviderInfo info,
+ IEnumerable<ChannelInfo> tunerChannels,
+ bool enableCache,
+ CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
@@ -367,8 +370,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!string.IsNullOrWhiteSpace(epgChannel.Name))
{
- //tunerChannel.Name = epgChannel.Name;
+ // tunerChannel.Name = epgChannel.Name;
}
+
if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
{
tunerChannel.ImageUrl = epgChannel.ImageUrl;
@@ -377,10 +381,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
- new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
-
- private async Task<EpgChannelData> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken)
+ private async Task<EpgChannelData> GetEpgChannels(
+ IListingsProvider provider,
+ ListingsProviderInfo info,
+ bool enableCache,
+ CancellationToken cancellationToken)
{
if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result))
{
@@ -398,59 +403,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return result;
}
- private class EpgChannelData
- {
- public EpgChannelData(List<ChannelInfo> channels)
- {
- ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
-
- foreach (var channel in channels)
- {
- ChannelsById[channel.Id] = channel;
-
- if (!string.IsNullOrEmpty(channel.Number))
- {
- ChannelsByNumber[channel.Number] = channel;
- }
-
- var normalizedName = NormalizeName(channel.Name ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(normalizedName))
- {
- ChannelsByName[normalizedName] = channel;
- }
- }
- }
-
- private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
- private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
- private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
-
- public ChannelInfo GetChannelById(string id)
- {
- ChannelInfo result = null;
-
- ChannelsById.TryGetValue(id, out result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByNumber(string number)
- {
- ChannelsByNumber.TryGetValue(number, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByName(string name)
- {
- ChannelsByName.TryGetValue(name, out var result);
-
- return result;
- }
- }
-
private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken)
{
var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false);
@@ -462,11 +414,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
foreach (NameValuePair mapping in mappings)
{
- if (StringHelper.EqualsIgnoreCase(mapping.Name, channelId))
+ if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
{
return mapping.Value;
}
}
+
return channelId;
}
@@ -480,7 +433,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
}
- private ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, EpgChannelData epgChannelData)
+ private ChannelInfo GetEpgChannelFromTunerChannel(
+ NameValuePair[] mappings,
+ ChannelInfo tunerChannel,
+ EpgChannelData epgChannelData)
{
if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
{
@@ -502,7 +458,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
{
var tunerChannelId = tunerChannel.TunerChannelId;
- if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
+ if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
{
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
}
@@ -541,7 +497,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
{
- var normalizedName = NormalizeName(tunerChannel.Name);
+ var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
var channel = epgChannelData.GetChannelByName(normalizedName);
@@ -554,11 +510,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return null;
}
- private static string NormalizeName(string value)
- {
- return value.Replace(" ", string.Empty).Replace("-", string.Empty);
- }
-
public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
@@ -604,6 +555,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
_seriesTimerProvider.Delete(remove);
}
+
return Task.CompletedTask;
}
@@ -648,11 +600,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();
@@ -663,16 +610,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
- public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
+ public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
{
- var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
+ var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
null :
- _timerProvider.GetTimerByProgramId(timer.ProgramId);
+ _timerProvider.GetTimerByProgramId(info.ProgramId);
if (existingTimer != null)
{
- if (existingTimer.Status == RecordingStatus.Cancelled ||
- existingTimer.Status == RecordingStatus.Completed)
+ if (existingTimer.Status == RecordingStatus.Cancelled
+ || existingTimer.Status == RecordingStatus.Completed)
{
existingTimer.Status = RecordingStatus.New;
existingTimer.IsManual = true;
@@ -685,39 +632,37 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- timer.Id = Guid.NewGuid().ToString("N");
+ info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
LiveTvProgram programInfo = null;
- if (!string.IsNullOrWhiteSpace(timer.ProgramId))
+ if (!string.IsNullOrWhiteSpace(info.ProgramId))
{
- programInfo = GetProgramInfoFromCache(timer);
+ programInfo = GetProgramInfoFromCache(info);
}
+
if (programInfo == null)
{
- _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
- programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
+ _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
+ programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
}
if (programInfo != null)
{
- CopyProgramInfoToTimerInfo(programInfo, timer);
+ CopyProgramInfoToTimerInfo(programInfo, info);
}
- timer.IsManual = true;
- _timerProvider.Add(timer);
+ info.IsManual = true;
+ _timerProvider.Add(info);
- if (TimerCreated != null)
- {
- TimerCreated(this, new GenericEventArgs<TimerInfo>(timer));
- }
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
- return Task.FromResult(timer.Id);
+ return Task.FromResult(info.Id);
}
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
{
- info.Id = Guid.NewGuid().ToString("N");
+ info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
// populate info.seriesID
var program = GetProgramInfoFromCache(info.ProgramId);
@@ -804,7 +749,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
// Only update if not currently active
- if (!_activeRecordings.TryGetValue(updatedTimer.Id, out var activeRecordingInfo))
+ if (!_activeRecordings.TryGetValue(updatedTimer.Id, out _))
{
existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
@@ -850,33 +795,31 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return info.Path;
}
- return null;
- }
- public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
- {
- return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
+ return null;
}
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{
- if (string.IsNullOrWhiteSpace(path))
+ if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty)
{
return null;
}
- foreach (var recording in _activeRecordings.Values)
+ foreach (var (_, recordingInfo) in _activeRecordings)
{
- if (string.Equals(recording.Path, path, StringComparison.Ordinal) && !recording.CancellationTokenSource.IsCancellationRequested)
+ if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal) && !recordingInfo.CancellationTokenSource.IsCancellationRequested)
{
- var timer = recording.Timer;
+ var timer = recordingInfo.Timer;
if (timer.Status != RecordingStatus.InProgress)
{
return null;
}
- return recording;
+
+ return recordingInfo;
}
}
+
return null;
}
@@ -968,18 +911,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
- List<ProgramInfo> programs;
-
if (epgChannel == null)
{
_logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
- programs = new List<ProgramInfo>();
+ continue;
}
- else
- {
- programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
+
+ List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false)).ToList();
- }
// Replace the value that came from the provider with a normalized value
foreach (var program in programs)
@@ -995,7 +934,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- return new List<ProgramInfo>();
+ return Enumerable.Empty<ProgramInfo>();
}
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
@@ -1058,23 +997,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") + "_" + mediaSource.Id;
-
- //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
- //{
- // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
- // ticks = Math.Max(0, ticks);
- // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
- //}
-
- return mediaSource;
- }
-
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(channelId))
@@ -1095,7 +1017,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (NotImplementedException)
{
-
}
}
@@ -1106,7 +1027,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var stream = new MediaSourceInfo
{
- EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+ EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
EncoderProtocol = MediaProtocol.Http,
Path = info.Path,
Protocol = MediaProtocol.File,
@@ -1122,7 +1043,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IgnoreIndex = true
};
- await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _config.CommonApplicationPaths)
+ await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return new List<MediaSourceInfo>
@@ -1146,7 +1067,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return Task.CompletedTask;
}
- async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
+ private async void OnTimerProviderTimerFired(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
@@ -1181,7 +1102,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (OperationCanceledException)
{
-
}
catch (Exception ex)
{
@@ -1225,7 +1145,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (timer.SeasonNumber.HasValue)
{
- folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+ folderName = string.Format(
+ CultureInfo.InvariantCulture,
+ "Season {0}",
+ timer.SeasonNumber.Value);
recordPath = Path.Combine(recordPath, folderName);
}
}
@@ -1279,6 +1202,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Sports");
}
+
recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
else
@@ -1287,6 +1211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Other");
}
+
recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
@@ -1308,6 +1233,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
programInfo = GetProgramInfoFromCache(timer);
}
+
if (programInfo == null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
@@ -1319,9 +1245,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CopyProgramInfoToTimerInfo(programInfo, timer);
}
- string seriesPath = null;
var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false);
- var recordPath = GetRecordingPath(timer, remoteMetadata, out seriesPath);
+ var recordPath = GetRecordingPath(timer, remoteMetadata, out string seriesPath);
var recordingStatus = RecordingStatus.New;
string liveStreamId = null;
@@ -1330,19 +1255,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- var allMediaSources = await _mediaSourceManager.GetPlayackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false);
+ var allMediaSources = await _mediaSourceManager.GetPlaybackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false);
var mediaStreamInfo = allMediaSources[0];
IDirectStreamProvider directStreamProvider = null;
if (mediaStreamInfo.RequiresOpening)
{
- var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(new LiveStreamRequest
- {
- ItemId = channelItem.Id,
- OpenToken = mediaStreamInfo.OpenToken
-
- }, CancellationToken.None).ConfigureAwait(false);
+ var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(
+ new LiveStreamRequest
+ {
+ ItemId = channelItem.Id,
+ OpenToken = mediaStreamInfo.OpenToken
+ },
+ CancellationToken.None).ConfigureAwait(false);
mediaStreamInfo = liveStreamResponse.Item1.MediaSource;
liveStreamId = mediaStreamInfo.LiveStreamId;
@@ -1360,7 +1286,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
- _logger.LogInformation("Writing file to path: " + recordPath);
+ _logger.LogInformation("Writing file to: {Path}", recordPath);
Action onStarted = async () =>
{
@@ -1376,7 +1302,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
await CreateRecordingFolders().ConfigureAwait(false);
TriggerRefresh(recordPath);
- EnforceKeepUpTo(timer, seriesPath);
+ await EnforceKeepUpTo(timer, seriesPath).ConfigureAwait(false);
};
await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
@@ -1416,12 +1342,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10)
{
- const int retryIntervalSeconds = 60;
- _logger.LogInformation("Retrying recording in {0} seconds.", retryIntervalSeconds);
+ const int RetryIntervalSeconds = 60;
+ _logger.LogInformation("Retrying recording in {0} seconds.", RetryIntervalSeconds);
timer.Status = RecordingStatus.New;
timer.PrePaddingSeconds = 0;
- timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
+ timer.StartDate = DateTime.UtcNow.AddSeconds(RetryIntervalSeconds);
timer.RetryCount++;
_timerProvider.AddOrUpdate(timer);
}
@@ -1485,24 +1411,26 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void TriggerRefresh(string path)
{
- _logger.LogInformation("Triggering refresh on {path}", path);
+ _logger.LogInformation("Triggering refresh on {Path}", path);
var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
if (item != null)
{
- _logger.LogInformation("Refreshing recording parent {path}", item.Path);
+ _logger.LogInformation("Refreshing recording parent {Path}", item.Path);
- _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
- {
- RefreshPaths = new string[]
+ _providerManager.QueueRefresh(
+ item.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
- path,
- Path.GetDirectoryName(path),
- Path.GetDirectoryName(Path.GetDirectoryName(path))
- }
-
- }, RefreshPriority.High);
+ RefreshPaths = new string[]
+ {
+ path,
+ Path.GetDirectoryName(path),
+ Path.GetDirectoryName(Path.GetDirectoryName(path))
+ }
+ },
+ RefreshPriority.High);
}
}
@@ -1524,7 +1452,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase))
{
var parentItem = item.GetParent();
- if (parentItem != null && !(parentItem is AggregateFolder))
+ if (parentItem != null && parentItem is not AggregateFolder)
{
item = parentItem;
}
@@ -1534,12 +1462,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return item;
}
- private async void EnforceKeepUpTo(TimerInfo timer, string seriesPath)
+ private async Task EnforceKeepUpTo(TimerInfo timer, string seriesPath)
{
if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
{
return;
}
+
if (string.IsNullOrWhiteSpace(seriesPath))
{
return;
@@ -1577,22 +1506,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
DeleteLibraryItemsForTimers(timersToDelete);
- var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
-
- if (librarySeries == null)
+ if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
{
return;
}
- var episodesToDelete = (librarySeries.GetItemList(new InternalItemsQuery
- {
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) },
- IsVirtualItem = false,
- IsFolder = false,
- Recursive = true,
- DtoOptions = new DtoOptions(true)
-
- }))
+ var episodesToDelete = librarySeries.GetItemList(
+ new InternalItemsQuery
+ {
+ OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true,
+ DtoOptions = new DtoOptions(true)
+ })
.Where(i => i.IsFileProtocol && File.Exists(i.Path))
.Skip(seriesTimer.KeepUpTo - 1)
.ToList();
@@ -1601,11 +1528,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
try
{
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, true);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ true);
}
catch (Exception ex)
{
@@ -1619,7 +1548,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
private void DeleteLibraryItemsForTimers(List<TimerInfo> timers)
{
foreach (var timer in timers)
@@ -1646,22 +1574,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (libraryItem != null)
{
- _libraryManager.DeleteItem(libraryItem, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, true);
+ _libraryManager.DeleteItem(
+ libraryItem,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ true);
}
- else
+ else if (File.Exists(timer.RecordingPath))
{
- try
- {
- _fileSystem.DeleteFile(timer.RecordingPath);
- }
- catch (IOException)
- {
-
- }
+ _fileSystem.DeleteFile(timer.RecordingPath);
}
_timerProvider.Delete(timer);
@@ -1692,26 +1615,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return true;
}
- var hasRecordingAtPath = _activeRecordings
- .Values
- .ToList()
- .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
-
- if (hasRecordingAtPath)
- {
- return true;
- }
- return false;
+ return _activeRecordings
+ .Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
}
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
- return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
+ return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _config);
}
- return new DirectRecorder(_logger, _httpClient, _fileSystem, _streamHelper);
+ return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
}
private void OnSuccessfulRecording(TimerInfo timer, string path)
@@ -1729,20 +1644,23 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- var process = _processFactory.Create(new ProcessOptions
+ var process = new Process
{
- Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
- CreateNoWindow = true,
- EnableRaisingEvents = true,
- ErrorDialog = false,
- FileName = options.RecordingPostProcessor,
- IsHidden = true,
- UseShellExecute = false
- });
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
+ CreateNoWindow = true,
+ ErrorDialog = false,
+ FileName = options.RecordingPostProcessor,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ UseShellExecute = false
+ },
+ EnableRaisingEvents = true
+ };
_logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
- process.Exited += Process_Exited;
+ process.Exited += OnProcessExited;
process.Start();
}
catch (Exception ex)
@@ -1756,19 +1674,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
}
- private void Process_Exited(object sender, EventArgs e)
+ private void OnProcessExited(object sender, EventArgs e)
{
- var process = (IProcess)sender;
- try
+ using (var process = (Process)sender)
{
_logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode);
}
- catch
- {
-
- }
-
- process.Dispose();
}
private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
@@ -1778,44 +1689,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
}
- string imageSaveFilenameWithoutExtension = null;
-
- switch (image.Type)
+ string imageSaveFilenameWithoutExtension = image.Type switch
{
- case ImageType.Primary:
-
- if (program.IsSeries)
- {
- imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
- }
- else
- {
- imageSaveFilenameWithoutExtension = "poster";
- }
-
- break;
- case ImageType.Logo:
- imageSaveFilenameWithoutExtension = "logo";
- break;
- case ImageType.Thumb:
- if (program.IsSeries)
- {
- imageSaveFilenameWithoutExtension = Path.GetFileNameWithoutExtension(recordingPath) + "-thumb";
- }
- else
- {
- imageSaveFilenameWithoutExtension = "landscape";
- }
-
- break;
- case ImageType.Backdrop:
- imageSaveFilenameWithoutExtension = "fanart";
- break;
- default:
- break;
- }
+ ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster",
+ ImageType.Logo => "logo",
+ ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscape",
+ ImageType.Backdrop => "fanart",
+ _ => null
+ };
- if (string.IsNullOrWhiteSpace(imageSaveFilenameWithoutExtension))
+ if (imageSaveFilenameWithoutExtension == null)
{
return;
}
@@ -1895,11 +1778,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
-
}).FirstOrDefault() as LiveTvProgram;
// dummy this up
@@ -1923,11 +1805,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
program.AddGenre("Sports");
}
+
if (timer.IsKids)
{
program.AddGenre("Kids");
program.AddGenre("Children");
}
+
if (timer.IsNews)
{
program.AddGenre("News");
@@ -1964,13 +1848,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
using (var writer = XmlWriter.Create(stream, settings))
@@ -1978,19 +1861,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteStartDocument(true);
writer.WriteStartElement("tvshow");
string id;
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id))
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id))
{
writer.WriteElementString("id", id);
}
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
+
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out id))
{
writer.WriteElementString("imdb_id", id);
}
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out id))
+
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out id))
{
writer.WriteElementString("tmdbid", id);
}
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id))
+
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out id))
{
writer.WriteElementString("zap2itid", id);
}
@@ -2016,7 +1902,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData)
{
var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
@@ -2026,13 +1911,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = _fileSystem.GetFileStream(nfoPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
var options = _config.GetNfoConfiguration();
@@ -2058,7 +1942,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("aired", premiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "aired",
+ premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
}
if (item.IndexNumber.HasValue)
@@ -2089,12 +1975,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var formatString = options.ReleaseDateFormat;
- writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
- writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString));
+ writer.WriteElementString(
+ "premiered",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "releasedate",
+ item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
}
}
- writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
+ writer.WriteElementString(
+ "dateadded",
+ DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
@@ -2108,7 +2000,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var overview = (item.Overview ?? string.Empty)
.StripHtml()
- .Replace("&quot;", "'");
+ .Replace("&quot;", "'", StringComparison.Ordinal);
writer.WriteElementString("plot", overview);
@@ -2150,14 +2042,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString("credits", person);
}
- var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
+ var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
if (!string.IsNullOrEmpty(tmdbCollection))
{
writer.WriteElementString("collectionnumber", tmdbCollection);
}
- var imdb = item.GetProviderId(MetadataProviders.Imdb);
+ var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdb))
{
if (!isSeriesEpisode)
@@ -2171,7 +2063,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
lockData = false;
}
- var tvdb = item.GetProviderId(MetadataProviders.Tvdb);
+ var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrEmpty(tvdb))
{
writer.WriteElementString("tvdbid", tvdb);
@@ -2180,7 +2072,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
lockData = false;
}
- var tmdb = item.GetProviderId(MetadataProviders.Tmdb);
+ var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
if (!string.IsNullOrEmpty(tmdb))
{
writer.WriteElementString("tmdbid", tmdb);
@@ -2216,17 +2108,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
private static bool IsPersonType(PersonInfo person, string type)
- {
- return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- }
-
- private void AddGenre(List<string> genres, string genre)
- {
- if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
- {
- genres.Add(genre);
- }
- }
+ => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
private LiveTvProgram GetProgramInfoFromCache(string programId)
{
@@ -2254,7 +2137,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
Limit = 1,
DtoOptions = new DtoOptions(true)
{
@@ -2262,7 +2145,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
},
MinStartDate = startDateUtc.AddMinutes(-3),
MaxStartDate = startDateUtc.AddMinutes(3),
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) }
+ OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }
};
if (!string.IsNullOrWhiteSpace(channelId))
@@ -2285,25 +2168,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return false;
}
- if (!seriesTimer.RecordAnyTime)
+ if (!seriesTimer.RecordAnyTime
+ && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
{
- if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks)
- {
- return true;
- }
+ return true;
}
- //if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
- //{
- // return true;
- //}
-
if (seriesTimer.RecordNewOnly && timer.IsRepeat)
{
return true;
}
- if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
+ if (!seriesTimer.RecordAnyChannel
+ && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -2348,18 +2225,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var allTimers = GetTimersForSeries(seriesTimer).ToList();
-
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
- var existingTimer = _timerProvider.GetTimer(timer.Id);
-
- if (existingTimer == null)
- {
- existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
+ var existingTimer = _timerProvider.GetTimer(timer.Id)
+ ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
? null
- : _timerProvider.GetTimerByProgramId(timer.ProgramId);
- }
+ : _timerProvider.GetTimerByProgramId(timer.ProgramId));
if (existingTimer == null)
{
@@ -2371,10 +2243,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
enabledTimersForSeries.Add(timer);
}
+
_timerProvider.Add(timer);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
}
+
// Only update if not currently active - test both new timer and existing in case Id's are different
// Id's could be different if the timer was created manually prior to series timer creation
else if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _))
@@ -2478,7 +2352,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true)
{
@@ -2510,13 +2384,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
{
- channel = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
- ItemIds = new[] { parent.ChannelId },
- DtoOptions = new DtoOptions()
-
- }).Cast<LiveTvChannel>().FirstOrDefault();
+ channel = _libraryManager.GetItemList(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
+ ItemIds = new[] { parent.ChannelId },
+ DtoOptions = new DtoOptions()
+ }).FirstOrDefault() as LiveTvChannel;
if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
@@ -2533,7 +2407,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var timer = new TimerInfo
{
ChannelId = channelId,
- Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N"),
+ Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N", CultureInfo.InvariantCulture),
StartDate = parent.StartDate,
EndDate = parent.EndDate.Value,
ProgramId = parent.ExternalId,
@@ -2569,13 +2443,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
{
- channel = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
- ItemIds = new[] { programInfo.ChannelId },
- DtoOptions = new DtoOptions()
-
- }).Cast<LiveTvChannel>().FirstOrDefault();
+ channel = _libraryManager.GetItemList(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
+ ItemIds = new[] { programInfo.ChannelId },
+ DtoOptions = new DtoOptions()
+ }).FirstOrDefault() as LiveTvChannel;
if (channel != null && !string.IsNullOrWhiteSpace(channel.ExternalId))
{
@@ -2620,10 +2494,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
foreach (var providerId in timerInfo.ProviderIds)
{
- var srch = "Series";
- if (providerId.Key.StartsWith(srch, StringComparison.OrdinalIgnoreCase))
+ const string Search = "Series";
+ if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase))
{
- seriesProviderIds[providerId.Key.Substring(srch.Length)] = providerId.Value;
+ seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value;
}
}
@@ -2634,12 +2508,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
{
- var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(Series).Name },
- Name = program.Name
-
- }).ToArray();
+ var seriesIds = _libraryManager.GetItemIds(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { nameof(Series) },
+ Name = program.Name
+ }).ToArray();
if (seriesIds.Length == 0)
{
@@ -2650,7 +2524,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var result = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds,
@@ -2668,59 +2542,70 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return false;
}
- private bool _disposed;
+ /// <inheritdoc />
public void Dispose()
{
- _disposed = true;
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _recordingDeleteSemaphore.Dispose();
+ }
+
foreach (var pair in _activeRecordings.ToList())
{
pair.Value.CancellationTokenSource.Cancel();
}
+
+ _disposed = true;
}
- public List<VirtualFolderInfo> GetRecordingFolders()
+ public IEnumerable<VirtualFolderInfo> GetRecordingFolders()
{
- var list = new List<VirtualFolderInfo>();
-
var defaultFolder = RecordingPath;
var defaultName = "Recordings";
if (Directory.Exists(defaultFolder))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { defaultFolder },
Name = defaultName
- });
+ };
}
var customPath = GetConfiguration().MovieRecordingPath;
- if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
- CollectionType = CollectionType.Movies
- });
+ CollectionType = CollectionTypeOptions.Movies
+ };
}
customPath = GetConfiguration().SeriesRecordingPath;
- if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
- list.Add(new VirtualFolderInfo
+ yield return new VirtualFolderInfo
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
- CollectionType = CollectionType.TvShows
- });
+ CollectionType = CollectionTypeOptions.TvShows
+ };
}
-
- return list;
}
- private const int TunerDiscoveryDurationMs = 3000;
-
public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
{
var list = new List<TunerHostInfo>();
@@ -2739,6 +2624,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
.ToList();
}
+
list.AddRange(discoveredDevices);
}
@@ -2775,11 +2661,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken)
+ private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
{
try
{
- var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false);
+ var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
foreach (var device in discoveredDevices)
{
@@ -2796,11 +2682,4 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
}
- public static class ConfigurationExtension
- {
- public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager)
- {
- return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
- }
- }
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 9a9bae215..835028b92 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -1,22 +1,25 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
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.Diagnostics;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -24,34 +27,26 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class EncodedRecorder : IRecorder
{
private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
+ private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
- private IProcess _process;
- private readonly IProcessFactory _processFactory;
- private readonly IJsonSerializer _json;
- private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
- private readonly IServerConfigurationManager _config;
+ private Process _process;
public EncodedRecorder(
ILogger logger,
- IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
- IJsonSerializer json,
- IProcessFactory processFactory,
- IServerConfigurationManager config)
+ IServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
- _fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
- _json = json;
- _processFactory = processFactory;
- _config = config;
+ _serverConfigurationManager = serverConfigurationManager;
}
private static bool CopySubtitles => false;
@@ -64,73 +59,63 @@ 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)
+ private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- var process = _processFactory.Create(new ProcessOptions
+ var processStartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
- // Must consume both stdout and stderr or deadlocks may occur
- //RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,
Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration),
- IsHidden = true,
- ErrorDialog = false,
- EnableRaisingEvents = true
- });
-
- _process = process;
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ };
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+ var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments;
_logger.LogInformation(commandLineLogMessage);
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- _logFileStream = _fileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
+ _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
+ await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
- process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile);
+ _process = new Process
+ {
+ StartInfo = processStartInfo,
+ EnableRaisingEvents = true
+ };
+ _process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
- process.Start();
+ _process.Start();
cancellationToken.Register(Stop);
- // MUST read both stdout and stderr asynchronously or a deadlock may occurr
- //process.BeginOutputReadLine();
-
onStarted();
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
- StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
+ _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
-
- return _taskCompletionSource.Task;
}
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration)
@@ -138,11 +123,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
string videoArgs;
if (EncodeVideo(mediaSource))
{
- var maxBitrate = 25000000;
+ const int MaxBitrate = 25000000;
videoArgs = string.Format(
- "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41",
- GetOutputSizeParam(),
- maxBitrate.ToString(CultureInfo.InvariantCulture));
+ CultureInfo.InvariantCulture,
+ "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41",
+ GetOutputSizeParam(),
+ MaxBitrate);
}
else
{
@@ -151,18 +137,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
videoArgs += " -fflags +genpts";
- var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
- durationParam = string.Empty;
-
var flags = new List<string>();
if (mediaSource.IgnoreDts)
{
flags.Add("+igndts");
}
+
if (mediaSource.IgnoreIndex)
{
flags.Add("+ignidx");
}
+
if (mediaSource.GenPtsInput)
{
flags.Add("+genpts");
@@ -172,11 +157,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (flags.Count > 0)
{
- inputModifier += " -fflags " + string.Join("", flags.ToArray());
+ inputModifier += " -fflags " + string.Join(string.Empty, flags);
}
- var videoStream = mediaSource.VideoStream;
-
if (mediaSource.ReadAtNativeFramerate)
{
inputModifier += " -re";
@@ -194,38 +177,38 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
- //var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ?
+ // var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ?
// " -f mp4 -movflags frag_keyframe+empty_moov" :
// string.Empty;
var outputParam = string.Empty;
- var commandLineArgs = string.Format("-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4}{6} -y \"{1}\"",
+ var threads = EncodingHelper.GetNumberOfThreads(null, _serverConfigurationManager.GetEncodingOptions(), null);
+ var commandLineArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
- targetFile,
+ targetFile.Replace("\"", "\\\""), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
- durationParam,
- outputParam);
+ outputParam,
+ threads);
return inputModifier + " " + commandLineArgs;
}
private static string GetAudioArgs(MediaSourceInfo mediaSource)
{
- var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
- var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty;
-
return "-codec:a:0 copy";
- //var audioChannels = 2;
- //var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
- //if (audioStream != null)
- //{
+ // var audioChannels = 2;
+ // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
+ // if (audioStream != null)
+ // {
// audioChannels = audioStream.Channels ?? audioChannels;
- //}
- //return "-codec:a:0 aac -strict experimental -ab 320000";
+ // }
+ // return "-codec:a:0 aac -strict experimental -ab 320000";
}
private static bool EncodeVideo(MediaSourceInfo mediaSource)
@@ -234,20 +217,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()
{
@@ -257,7 +227,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
_logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath);
- //process.Kill();
_process.StandardInput.WriteLine("q");
}
catch (Exception ex)
@@ -305,17 +274,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Processes the exited.
/// </summary>
- private void OnFfMpegProcessExited(IProcess process, string inputFile)
+ private void OnFfMpegProcessExited(Process process)
{
- _hasExited = true;
+ using (process)
+ {
+ _hasExited = true;
- DisposeLogStream();
+ _logFileStream?.Dispose();
+ _logFileStream = null;
- try
- {
var exitCode = process.ExitCode;
- _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {path}", exitCode, _targetPath);
+ _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath);
if (exitCode == 0)
{
@@ -323,55 +293,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
- _taskCompletionSource.TrySetException(new Exception(string.Format("Recording for {path} failed. Exit code {ExitCode}", _targetPath, exitCode)));
- }
- }
- catch
- {
- _logger.LogError("FFMpeg recording exited with an error for {path}.", _targetPath);
- _taskCompletionSource.TrySetException(new Exception(string.Format("Recording for {path} failed", _targetPath)));
- }
- }
-
- private void DisposeLogStream()
- {
- if (_logFileStream != null)
- {
- try
- {
- _logFileStream.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing recording log stream");
+ _taskCompletionSource.TrySetException(
+ new Exception(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Recording for {0} failed. Exit code {1}",
+ _targetPath,
+ exitCode)));
}
-
- _logFileStream = null;
}
}
- private async void StartStreamingLog(Stream source, Stream target)
+ private async Task StartStreamingLog(Stream source, Stream target)
{
try
{
using (var reader = new StreamReader(source))
{
- while (!reader.EndOfStream)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
- await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false);
await target.FlushAsync().ConfigureAwait(false);
}
}
}
- catch (ObjectDisposedException)
- {
- // TODO Investigate and properly fix.
- // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
- }
catch (Exception ex)
{
_logger.LogError(ex, "Error reading ffmpeg recording log");
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
index 9c9ba09f5..a2ec2df37 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
@@ -1,15 +1,19 @@
+#pragma warning disable CS1591
+
using System.Threading.Tasks;
using MediaBrowser.Controller.Plugins;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
- public class EntryPoint : IServerEntryPoint
+ public sealed class EntryPoint : IServerEntryPoint
{
+ /// <inheritdoc />
public Task RunAsync()
{
return EmbyTV.Current.Start();
}
+ /// <inheritdoc />
public void Dispose()
{
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
new file mode 100644
index 000000000..20a8213a7
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -0,0 +1,54 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.LiveTv;
+
+namespace Emby.Server.Implementations.LiveTv.EmbyTV
+{
+ internal class EpgChannelData
+ {
+ private readonly Dictionary<string, ChannelInfo> _channelsById;
+
+ private readonly Dictionary<string, ChannelInfo> _channelsByNumber;
+
+ private readonly Dictionary<string, ChannelInfo> _channelsByName;
+
+ public EpgChannelData(IEnumerable<ChannelInfo> channels)
+ {
+ _channelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ _channelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ _channelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var channel in channels)
+ {
+ _channelsById[channel.Id] = channel;
+
+ if (!string.IsNullOrEmpty(channel.Number))
+ {
+ _channelsByNumber[channel.Number] = channel;
+ }
+
+ var normalizedName = NormalizeName(channel.Name ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(normalizedName))
+ {
+ _channelsByName[normalizedName] = channel;
+ }
+ }
+ }
+
+ public ChannelInfo? GetChannelById(string id)
+ => _channelsById.GetValueOrDefault(id);
+
+ public ChannelInfo? GetChannelByNumber(string number)
+ => _channelsByNumber.GetValueOrDefault(number);
+
+ public ChannelInfo? GetChannelByName(string name)
+ => _channelsByName.GetValueOrDefault(name);
+
+ public static string NormalizeName(string value)
+ {
+ return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
index 6eced3050..7705132da 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -11,7 +13,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Records the specified media source.
/// </summary>
- Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
+ /// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param>
+ /// <param name="mediaSource">The media source.</param>
+ /// <param name="targetFile">The target file.</param>
+ /// <param name="duration">The duration to record.</param>
+ /// <param name="onStarted">An action to perform when recording starts.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A <see cref="Task"/> that represents the recording operation.</returns>
+ Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index a2ac60b31..46979bfc5 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,9 +1,12 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
+using System.Text.Json;
+using Jellyfin.Extensions.Json;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -11,72 +14,71 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class ItemDataProvider<T>
where T : class
{
- private readonly object _fileDataLock = new object();
- private List<T> _items;
- private readonly IJsonSerializer _jsonSerializer;
- protected readonly ILogger Logger;
private readonly string _dataPath;
- protected readonly Func<T, T, bool> EqualityComparer;
+ private readonly object _fileDataLock = new object();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private T[]? _items;
- public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
+ public ItemDataProvider(
+ ILogger logger,
+ string dataPath,
+ Func<T, T, bool> equalityComparer)
{
Logger = logger;
_dataPath = dataPath;
EqualityComparer = equalityComparer;
- _jsonSerializer = jsonSerializer;
}
- public IReadOnlyList<T> GetAll()
- {
- lock (_fileDataLock)
- {
- if (_items == null)
- {
- Logger.LogInformation("Loading live tv data from {0}", _dataPath);
- _items = GetItemsFromFile(_dataPath);
- }
- return _items.ToList();
- }
- }
+ protected ILogger Logger { get; }
- private List<T> GetItemsFromFile(string path)
- {
- var jsonFile = path + ".json";
+ protected Func<T, T, bool> EqualityComparer { get; }
- if (!File.Exists(jsonFile))
+ [MemberNotNull(nameof(_items))]
+ private void EnsureLoaded()
+ {
+ if (_items != null)
{
- return new List<T>();
+ return;
}
- try
- {
- return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile) ?? new List<T>();
- }
- catch (IOException)
+ if (File.Exists(_dataPath))
{
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error deserializing {jsonFile}", jsonFile);
+ Logger.LogInformation("Loading live tv data from {Path}", _dataPath);
+
+ try
+ {
+ var bytes = File.ReadAllBytes(_dataPath);
+ _items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
+ if (_items == null)
+ {
+ Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
+ _items = Array.Empty<T>();
+ }
+
+ return;
+ }
+ catch (JsonException ex)
+ {
+ Logger.LogError(ex, "Error deserializing {Path}", _dataPath);
+ }
}
- return new List<T>();
+ _items = Array.Empty<T>();
}
- private void UpdateList(List<T> newList)
+ private void SaveList()
{
- if (newList == null)
- {
- throw new ArgumentNullException(nameof(newList));
- }
-
- var file = _dataPath + ".json";
- Directory.CreateDirectory(Path.GetDirectoryName(file));
+ Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
+ var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
+ File.WriteAllText(_dataPath, jsonString);
+ }
+ public IReadOnlyList<T> GetAll()
+ {
lock (_fileDataLock)
{
- _jsonSerializer.SerializeToFile(newList, file);
- _items = newList;
+ EnsureLoaded();
+ return (T[])_items.Clone();
}
}
@@ -87,18 +89,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(item));
}
- var list = GetAll().ToList();
-
- var index = list.FindIndex(i => EqualityComparer(i, item));
-
- if (index == -1)
+ lock (_fileDataLock)
{
- throw new ArgumentException("item not found");
- }
+ EnsureLoaded();
- list[index] = item;
+ var index = Array.FindIndex(_items, i => EqualityComparer(i, item));
+ if (index == -1)
+ {
+ throw new ArgumentException("item not found");
+ }
- UpdateList(list);
+ _items[index] = item;
+
+ SaveList();
+ }
}
public virtual void Add(T item)
@@ -108,37 +112,58 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(item));
}
- var list = GetAll().ToList();
-
- if (list.Any(i => EqualityComparer(i, item)))
+ lock (_fileDataLock)
{
- throw new ArgumentException("item already exists");
- }
+ EnsureLoaded();
- list.Add(item);
+ if (_items.Any(i => EqualityComparer(i, item)))
+ {
+ throw new ArgumentException("item already exists", nameof(item));
+ }
+
+ int oldLen = _items.Length;
+ var newList = new T[oldLen + 1];
+ _items.CopyTo(newList, 0);
+ newList[oldLen] = item;
+ _items = newList;
- UpdateList(list);
+ SaveList();
+ }
}
- public void AddOrUpdate(T item)
+ public virtual void AddOrUpdate(T item)
{
- var list = GetAll().ToList();
-
- if (!list.Any(i => EqualityComparer(i, item)))
- {
- Add(item);
- }
- else
+ lock (_fileDataLock)
{
- Update(item);
+ EnsureLoaded();
+
+ int index = Array.FindIndex(_items, i => EqualityComparer(i, item));
+ if (index == -1)
+ {
+ int oldLen = _items.Length;
+ var newList = new T[oldLen + 1];
+ _items.CopyTo(newList, 0);
+ newList[oldLen] = item;
+ _items = newList;
+ }
+ else
+ {
+ _items[index] = item;
+ }
+
+ SaveList();
}
}
public virtual void Delete(T item)
{
- var list = GetAll().Where(i => !EqualityComparer(i, item)).ToList();
+ lock (_fileDataLock)
+ {
+ EnsureLoaded();
+ _items = _items.Where(i => !EqualityComparer(i, item)).ToArray();
- UpdateList(list);
+ SaveList();
+ }
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
new file mode 100644
index 000000000..83f5e8413
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.LiveTv.EmbyTV
+{
+ /// <summary>
+ /// Class containing extension methods for working with the nfo configuration.
+ /// </summary>
+ public static class NfoConfigurationExtensions
+ {
+ /// <summary>
+ /// Gets the nfo configuration.
+ /// </summary>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <returns>The nfo configuration.</returns>
+ public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager)
+ => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index ded3c7607..32245f899 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -1,10 +1,12 @@
+#pragma warning disable CS1591
+
using System;
using System.Globalization;
using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
- internal class RecordingHelper
+ internal static class RecordingHelper
{
public static DateTime GetStartTime(TimerInfo timer)
{
@@ -21,7 +23,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
{
- name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
+ name += string.Format(
+ CultureInfo.InvariantCulture,
+ " S{0}E{1}",
+ info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture),
+ info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
addHyphen = false;
}
else if (info.OriginalAirDate.HasValue)
@@ -32,7 +38,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
- name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd");
+ name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
}
else
@@ -50,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
name += " " + info.EpisodeTitle;
}
}
-
else if (info.IsMovie && info.ProductionYear != null)
{
name += " (" + info.ProductionYear + ")";
@@ -65,16 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static string GetDateString(DateTime date)
{
- date = date.ToLocalTime();
-
- return string.Format("{0}_{1}_{2}_{3}_{4}_{5}",
- date.Year.ToString("0000", CultureInfo.InvariantCulture),
- date.Month.ToString("00", CultureInfo.InvariantCulture),
- date.Day.ToString("00", CultureInfo.InvariantCulture),
- date.Hour.ToString("00", CultureInfo.InvariantCulture),
- date.Minute.ToString("00", CultureInfo.InvariantCulture),
- date.Second.ToString("00", CultureInfo.InvariantCulture)
- );
+ return date.ToLocalTime().ToString("yyyy_MM_dd_HH_mm_ss", CultureInfo.InvariantCulture);
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
index 520b44404..b1259de23 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -1,17 +1,19 @@
+#pragma warning disable CS1591
+
using System;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
{
- public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
- : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ public SeriesTimerManager(ILogger logger, string dataPath)
+ : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
+ /// <inheritdoc />
public override void Add(SeriesTimerInfo item)
{
if (string.IsNullOrEmpty(item.Id))
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 3c807a8ea..a861e6ae4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -1,12 +1,13 @@
+#pragma warning disable CS1591
+
using System;
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;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -14,21 +15,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class TimerManager : ItemDataProvider<TimerInfo>
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
- private readonly ILogger _logger;
-
- public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
- public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
- : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ public TimerManager(ILogger logger, string dataPath)
+ : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
- _logger = logger1;
}
+ public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired;
+
public void RestartTimers()
{
StopTimers();
- foreach (var item in GetAll().ToList())
+ foreach (var item in GetAll())
{
AddOrUpdateSystemTimer(item);
}
@@ -64,16 +63,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- var list = GetAll().ToList();
+ base.AddOrUpdate(item);
+ }
- if (!list.Any(i => EqualityComparer(i, item)))
- {
- base.Add(item);
- }
- else
- {
- base.Update(item);
- }
+ public override void AddOrUpdate(TimerInfo item)
+ {
+ base.AddOrUpdate(item);
+ AddOrUpdateSystemTimer(item);
}
public override void Add(TimerInfo item)
@@ -89,8 +85,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static bool ShouldStartTimer(TimerInfo item)
{
- if (item.Status == RecordingStatus.Completed ||
- item.Status == RecordingStatus.Cancelled)
+ if (item.Status == RecordingStatus.Completed
+ || item.Status == RecordingStatus.Cancelled)
{
return false;
}
@@ -112,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (startDate < now)
{
- TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = item });
+ TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(item));
return;
}
@@ -126,12 +122,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (_timers.TryAdd(item.Id, timer))
{
- _logger.LogInformation("Creating recording timer for {id}, {name}. Timer will fire in {minutes} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ Logger.LogInformation(
+ "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes",
+ item.Id,
+ item.Name,
+ dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
}
else
{
timer.Dispose();
- _logger.LogWarning("Timer already exists for item {id}", item.Id);
+ Logger.LogWarning("Timer already exists for item {Id}", item.Id);
}
}
@@ -143,23 +143,23 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private void TimerCallback(object state)
+ private void TimerCallback(object? state)
{
- var timerId = (string)state;
+ var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state));
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (timer != null)
{
- TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = timer });
+ TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
}
}
- public TimerInfo GetTimer(string id)
+ public TimerInfo? GetTimer(string id)
{
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
}
- public TimerInfo GetTimerByProgramId(string programId)
+ public TimerInfo? GetTimerByProgramId(string programId)
{
return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 0bbffb824..1f963e4a2 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -1,44 +1,61 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
+using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.Listings
{
public class SchedulesDirect : IListingsProvider
{
- private readonly ILogger _logger;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
+ private readonly ILogger<SchedulesDirect> _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
- private readonly IApplicationHost _appHost;
+ private readonly ICryptoProvider _cryptoProvider;
- private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+ private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private DateTime _lastErrorResponse;
- public SchedulesDirect(ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IApplicationHost appHost)
+ public SchedulesDirect(
+ ILogger<SchedulesDirect> logger,
+ IHttpClientFactory httpClientFactory,
+ ICryptoProvider cryptoProvider)
{
_logger = logger;
- _jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
- _appHost = appHost;
+ _httpClientFactory = httpClientFactory;
+ _cryptoProvider = cryptoProvider;
}
- private string UserAgent => _appHost.ApplicationUserAgent;
+ /// <inheritdoc />
+ public string Name => "Schedules Direct";
+
+ /// <inheritdoc />
+ public string Type => nameof(SchedulesDirect);
private static List<string> GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc)
{
@@ -49,7 +66,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);
}
@@ -78,173 +95,170 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
- var requestList = new List<ScheduleDirect.RequestScheduleForChannel>()
+ var requestList = new List<RequestScheduleForChannelDto>()
{
- new ScheduleDirect.RequestScheduleForChannel()
+ new RequestScheduleForChannelDto()
{
- stationID = channelId,
- date = dates
+ StationId = channelId,
+ Date = dates
}
};
- var requestString = _jsonSerializer.SerializeToString(requestList);
+ var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
- var httpOptions = new HttpRequestOptions()
+ 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(cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (dailySchedules == null)
{
- Url = ApiUrl + "/schedules",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- // The data can be large so give it some extra time
- TimeoutMs = 60000,
- LogErrorResponseBody = true,
- RequestContent = requestString
- };
-
- httpOptions.RequestHeaders["token"] = token;
-
- using (var response = await Post(httpOptions, true, info).ConfigureAwait(false))
- using (var reader = new StreamReader(response.Content))
- {
- var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false);
- _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
+ return Array.Empty<ProgramInfo>();
+ }
- httpOptions = new HttpRequestOptions()
- {
- Url = ApiUrl + "/programs",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true,
- // The data can be large so give it some extra time
- TimeoutMs = 60000
- };
+ _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);
- var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
- httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]";
+ var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
+ programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
+ programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false))
- using (var innerReader = new StreamReader(innerResponse.Content))
- {
- var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false);
- var programDict = programDetails.ToDictionary(p => p.programID, y => y);
+ using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
+ await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (programDetails == null)
+ {
+ return Array.Empty<ProgramInfo>();
+ }
- var programIdsWithImages =
- programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
- .ToList();
+ var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
- var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
+ var programIdsWithImages = programDetails
+ .Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
+ .ToList();
- 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 images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
- if (images != null)
- {
- var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
- if (imageIndex > -1)
- {
- var programEntry = programDict[schedule.programID];
+ var programsInfo = new List<ProgramInfo>();
+ foreach (ProgramDto 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 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));
+ if (string.IsNullOrEmpty(schedule.ProgramId))
+ {
+ continue;
+ }
- const double desiredAspect = 0.666666667;
+ if (images != null)
+ {
+ var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
+ if (imageIndex > -1)
+ {
+ var programEntry = programDict[schedule.ProgramId];
- programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, desiredAspect) ??
- GetProgramImage(ApiUrl, allImages, true, desiredAspect);
+ var allImages = images[imageIndex].Data;
+ 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 wideAspect = 1.77777778;
+ const double DesiredAspect = 2.0 / 3;
- programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, wideAspect);
+ programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
+ GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
- // Don't supply the same image twice
- if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
- {
- programEntry.thumbImage = null;
- }
+ const double WideAspect = 16.0 / 9;
- programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, wideAspect);
+ programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, 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);
+
+ // 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);
}
- return programsInfo;
}
+
+ programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.ProgramId]));
}
+
+ return programsInfo;
}
- private static int GetSizeOrder(ScheduleDirect.ImageData image)
+ private static int GetSizeOrder(ImageDataDto image)
{
- if (!string.IsNullOrWhiteSpace(image.height))
+ if (int.TryParse(image.Height, out int value))
{
- if (int.TryParse(image.height, out int value))
- {
- return value;
- }
+ return value;
}
return 0;
}
- private static string GetChannelNumber(ScheduleDirect.Map map)
+ private static string GetChannelNumber(MapDto map)
{
- var channelNumber = map.logicalChannelNumber;
+ var channelNumber = map.LogicalChannelNumber;
if (string.IsNullOrWhiteSpace(channelNumber))
{
- channelNumber = map.channel;
+ channelNumber = map.Channel;
}
+
if (string.IsNullOrWhiteSpace(channelNumber))
{
- channelNumber = map.atscMajor + "." + map.atscMinor;
+ channelNumber = map.AtscMajor + "." + map.AtscMinor;
}
return channelNumber.TrimStart('0');
}
- private static bool IsMovie(ScheduleDirect.ProgramDetails programInfo)
+ private static bool IsMovie(ProgramDetailsDto programInfo)
{
- return string.Equals(programInfo.entityType, "movie", StringComparison.OrdinalIgnoreCase);
+ return string.Equals(programInfo.EntityType, "movie", StringComparison.OrdinalIgnoreCase);
}
- private ProgramInfo GetProgram(string channelId, ScheduleDirect.Program programInfo, ScheduleDirect.ProgramDetails details)
+ private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
{
- var startAt = GetDate(programInfo.airDateTime);
- var endAt = startAt.AddSeconds(programInfo.duration);
+ if (programInfo.AirDateTime == null)
+ {
+ return null;
+ }
+
+ var startAt = programInfo.AirDateTime.Value;
+ var endAt = startAt.AddSeconds(programInfo.Duration);
var audioType = ProgramAudio.Stereo;
- var programId = programInfo.programID ?? string.Empty;
+ var programId = programInfo.ProgramId ?? string.Empty;
string newID = programId + "T" + startAt.Ticks + "C" + channelId;
- if (programInfo.audioProperties != null)
+ if (programInfo.AudioProperties.Count != 0)
{
- if (programInfo.audioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
+ if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Atmos;
}
- else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.audioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Stereo;
}
@@ -255,9 +269,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
string episodeTitle = null;
- if (details.episodeTitle150 != null)
+ if (details.EpisodeTitle150 != null)
{
- episodeTitle = details.episodeTitle150;
+ episodeTitle = details.EpisodeTitle150;
}
var info = new ProgramInfo
@@ -266,22 +280,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Id = newID,
StartDate = startAt,
EndDate = endAt,
- Name = details.titles[0].title120 ?? "Unkown",
+ Name = details.Titles[0].Title120 ?? "Unknown",
OfficialRating = null,
CommunityRating = null,
EpisodeTitle = episodeTitle,
Audio = audioType,
- //IsNew = programInfo.@new ?? false,
- IsRepeat = programInfo.@new == null,
- IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase),
- ImageUrl = details.primaryImage,
- ThumbImageUrl = details.thumbImage,
- IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase),
- IsSports = string.Equals(details.entityType, "sports", StringComparison.OrdinalIgnoreCase),
+ // IsNew = programInfo.@new ?? false,
+ IsRepeat = programInfo.New == null,
+ IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase),
+ ImageUrl = details.PrimaryImage,
+ ThumbImageUrl = details.ThumbImage,
+ IsKids = string.Equals(details.Audience, "children", StringComparison.OrdinalIgnoreCase),
+ IsSports = string.Equals(details.EntityType, "sports", StringComparison.OrdinalIgnoreCase),
IsMovie = IsMovie(details),
- Etag = programInfo.md5,
- IsLive = string.Equals(programInfo.liveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
- IsPremiere = programInfo.premiere || (programInfo.isPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
+ Etag = programInfo.Md5,
+ IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
+ IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
};
var showId = programId;
@@ -304,15 +318,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
info.ShowId = showId;
- if (programInfo.videoProperties != null)
+ if (programInfo.VideoProperties != null)
{
- info.IsHD = programInfo.videoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
- info.Is3D = programInfo.videoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
+ info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
+ info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
}
- if (details.contentRating != null && details.contentRating.Count > 0)
+ if (details.ContentRating != null && details.ContentRating.Count > 0)
{
- info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-").Replace("--", "-");
+ info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal)
+ .Replace("--", "-", StringComparison.Ordinal);
var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase))
@@ -321,15 +336,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- if (details.descriptions != null)
+ if (details.Descriptions != null)
{
- if (details.descriptions.description1000 != null && details.descriptions.description1000.Count > 0)
+ if (details.Descriptions.Description1000 != null && details.Descriptions.Description1000.Count > 0)
{
- info.Overview = details.descriptions.description1000[0].description;
+ info.Overview = details.Descriptions.Description1000[0].Description;
}
- else if (details.descriptions.description100 != null && details.descriptions.description100.Count > 0)
+ else if (details.Descriptions.Description100 != null && details.Descriptions.Description100.Count > 0)
{
- info.Overview = details.descriptions.description100[0].description;
+ info.Overview = details.Descriptions.Description100[0].Description;
}
}
@@ -337,20 +352,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
info.SeriesId = programId.Substring(0, 10);
- info.SeriesProviderIds[MetadataProviders.Zap2It.ToString()] = info.SeriesId;
+ info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId;
- if (details.metadata != null)
+ if (details.Metadata != null)
{
- foreach (var metadataProgram in details.metadata)
+ foreach (var metadataProgram in details.Metadata)
{
var gracenote = metadataProgram.Gracenote;
if (gracenote != null)
{
- info.SeasonNumber = gracenote.season;
+ info.SeasonNumber = gracenote.Season;
- if (gracenote.episode > 0)
+ if (gracenote.Episode > 0)
{
- info.EpisodeNumber = gracenote.episode;
+ info.EpisodeNumber = gracenote.Episode;
}
break;
@@ -359,24 +374,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- if (!string.IsNullOrWhiteSpace(details.originalAirDate))
+ if (details.OriginalAirDate != null)
{
- info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
+ info.OriginalAirDate = details.OriginalAirDate;
info.ProductionYear = info.OriginalAirDate.Value.Year;
}
- if (details.movie != null)
+ 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;
}
}
- if (details.genres != null)
+ if (details.Genres != null)
{
- info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
- info.IsNews = details.genres.Contains("news", StringComparer.OrdinalIgnoreCase);
+ info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
+ info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase);
if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase))
{
@@ -387,22 +403,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info;
}
- private static DateTime GetDate(string value)
- {
- var date = DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", CultureInfo.InvariantCulture);
-
- if (date.Kind != DateTimeKind.Utc)
- {
- date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
- }
- return date;
- }
-
- private string GetProgramImage(string apiUrl, IEnumerable<ScheduleDirect.ImageData> images, bool returnDefaultImage, double desiredAspect)
+ private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
{
var match = images
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
- .ThenByDescending(GetSizeOrder)
+ .ThenByDescending(i => GetSizeOrder(i))
.FirstOrDefault();
if (match == null)
@@ -410,7 +415,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null;
}
- var uri = match.uri;
+ var uri = match.Uri;
if (string.IsNullOrWhiteSpace(uri))
{
@@ -426,19 +431,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- private static double GetAspectRatio(ScheduleDirect.ImageData i)
+ private static double GetAspectRatio(ImageDataDto i)
{
int width = 0;
int height = 0;
- if (!string.IsNullOrWhiteSpace(i.width))
+ if (!string.IsNullOrWhiteSpace(i.Width))
{
- int.TryParse(i.width, out width);
+ _ = int.TryParse(i.Width, out width);
}
- if (!string.IsNullOrWhiteSpace(i.height))
+ if (!string.IsNullOrWhiteSpace(i.Height))
{
- int.TryParse(i.height, out height);
+ _ = int.TryParse(i.Height, out height);
}
if (height == 0 || width == 0)
@@ -451,60 +456,50 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return result;
}
- private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
+ private async Task<IReadOnlyList<ShowImagesDto>> GetImageForPrograms(
ListingsProviderInfo info,
- List<string> programIds,
- CancellationToken cancellationToken)
+ IReadOnlyList<string> programIds,
+ CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
- return new List<ScheduleDirect.ShowImages>();
+ return Array.Empty<ShowImagesDto>();
}
- var imageIdString = "[";
-
- foreach (var i in programIds)
+ StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
+ foreach (ReadOnlySpan<char> i in programIds)
{
- var imageId = i.Substring(0, 10);
-
- if (!imageIdString.Contains(imageId))
- {
- imageIdString += "\"" + imageId + "\",";
- }
+ str.Append('"')
+ .Append(i.Slice(0, 10))
+ .Append("\",");
}
- imageIdString = imageIdString.TrimEnd(',') + "]";
+ // Remove last ,
+ str.Length--;
+ str.Append(']');
- var httpOptions = new HttpRequestOptions()
+ using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
{
- Url = ApiUrl + "/metadata/programs",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- RequestContent = imageIdString,
- LogErrorResponseBody = true,
- // The data can be large so give it some extra time
- TimeoutMs = 60000
+ Content = new StringContent(str.ToString(), 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(cancellationToken).ConfigureAwait(false);
+ return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting image info from schedules direct");
- return new List<ScheduleDirect.ShowImages>();
+ return Array.Empty<ShowImagesDto>();
}
}
public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string country, string location, CancellationToken cancellationToken)
{
- var token = await GetToken(info, cancellationToken);
+ var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
var lineups = new List<NameIdPair>();
@@ -513,41 +508,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(cancellationToken).ConfigureAwait(false);
+
+ var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (root != null)
+ if (root != null)
+ {
+ foreach (HeadendsDto headend in root)
{
- foreach (ScheduleDirect.Headends headend in root)
+ foreach (LineupDto 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?[18..]
+ });
}
}
- else
- {
- _logger.LogInformation("No lineups available");
- }
+ }
+ else
+ {
+ _logger.LogInformation("No lineups available");
}
}
catch (Exception ex)
@@ -558,8 +545,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return lineups;
}
- private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
- private DateTime _lastErrorResponse;
private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
{
var username = info.Username;
@@ -582,8 +567,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null;
}
- NameValuePair savedToken = null;
- if (!_tokens.TryGetValue(username, out savedToken))
+ if (!_tokens.TryGetValue(username, out NameValuePair savedToken))
{
savedToken = new NameValuePair();
_tokens.TryAdd(username, savedToken);
@@ -609,7 +593,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
return result;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue)
{
@@ -619,6 +603,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_lastErrorResponse = DateTime.UtcNow;
}
}
+
throw;
}
finally
@@ -627,112 +612,64 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- private async Task<HttpResponseInfo> Post(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.EnableHttpCompression = true;
-
- // On windows 7 under .net core, this header is not getting added
-#if NETSTANDARD2_0
- if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
+ if (response.IsSuccessStatusCode)
{
- options.RequestHeaders["Accept-Encoding"] = "deflate";
+ return response;
}
-#endif
- try
+ // Response is automatically disposed in the calling function,
+ // so dispose manually if not returning.
+ response.Dispose();
+ if (!enableRetry || (int)response.StatusCode >= 500)
{
- return await _httpClient.Post(options).ConfigureAwait(false);
+ throw new HttpRequestException(
+ string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
+ null,
+ response.StatusCode);
}
- 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);
+ _tokens.Clear();
+ options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
+ return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
}
- private async Task<HttpResponseInfo> Get(HttpRequestOptions options,
- bool enableRetry,
- ListingsProviderInfo providerInfo)
- {
- // Schedules direct requires that the client support compression and will return a 400 response without it
- options.EnableHttpCompression = true;
-
- // On windows 7 under .net core, this header is not getting added
-#if NETSTANDARD2_0
- if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- {
- options.RequestHeaders["Accept-Encoding"] = "deflate";
- }
-#endif
-
- try
- {
- return await _httpClient.SendAsync(options, "GET").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 Get(options, false, providerInfo).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");
+ var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
+ // TODO: remove ToLower when Convert.ToHexString supports lowercase
+ // Schedules Direct requires the hex to be lowercase
+ string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
+ options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", 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);
+ response.EnsureSuccessStatusCode();
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
- 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: {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)
{
- var token = await GetToken(info, cancellationToken);
+ var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(token))
{
@@ -746,27 +683,11 @@ 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 (var response = await _httpClient.SendAsync(httpOptions, "PUT"))
- {
- }
+ 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);
}
- public string Name => "Schedules Direct";
-
- public static string TypeName = "SchedulesDirect";
- public string Type => TypeName;
-
private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(info.ListingsId))
@@ -774,7 +695,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
throw new ArgumentException("Listings Id required");
}
- var token = await GetToken(info, cancellationToken);
+ var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(token))
{
@@ -783,30 +704,23 @@ 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);
+ httpResponse.EnsureSuccessStatusCode();
+ await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ using var response = httpResponse.Content;
+ var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).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)) ?? false;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
- // Apparently we're supposed to swallow this
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ // SchedulesDirect returns 400 if no lineups are configured.
+ if (ex.StatusCode is HttpStatusCode.BadRequest)
{
return false;
}
@@ -823,11 +737,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
throw new ArgumentException("Username is required");
}
+
if (string.IsNullOrEmpty(info.Password))
{
throw new ArgumentException("Password is required");
}
}
+
if (validateListings)
{
if (string.IsNullOrEmpty(info.ListingsId))
@@ -857,388 +773,62 @@ namespace Emby.Server.Implementations.LiveTv.Listings
throw new Exception("ListingsId required");
}
- var token = await GetToken(info, cancellationToken);
+ var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(token))
{
throw new Exception("token required");
}
- var httpOptions = new HttpRequestOptions()
+ using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
+ options.Headers.TryAddWithoutValidation("token", token);
+
+ using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+ await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (root == null)
{
- Url = ApiUrl + "/lineups/" + listingsId,
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true,
- // The data can be large so give it some extra time
- TimeoutMs = 60000
- };
+ return new List<ChannelInfo>();
+ }
- httpOptions.RequestHeaders["token"] = token;
+ _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count);
+ _logger.LogInformation("Mapping Stations to Channel");
- var list = new List<ChannelInfo>();
+ var allStations = root.Stations;
- using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false))
- using (var response = httpResponse.Content)
+ var map = root.Map;
+ var list = new List<ChannelInfo>(map.Count);
+ foreach (var channel in map)
{
- 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 channelNumber = GetChannelNumber(channel);
- var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
+ var stationIndex = allStations.FindIndex(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase));
+ var station = stationIndex == -1
+ ? new StationDto { StationId = channel.StationId }
+ : allStations[stationIndex];
- foreach (ScheduleDirect.Map map in root.map)
+ var channelInfo = new ChannelInfo
{
- var channelNumber = GetChannelNumber(map);
-
- var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
- if (station == null)
- {
- station = new ScheduleDirect.Station
- {
- stationID = map.stationID
- };
- }
-
- var channelInfo = new ChannelInfo
- {
- Id = station.stationID,
- CallSign = station.callsign,
- Number = channelNumber,
- Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
- };
-
- if (station.logo != null)
- {
- channelInfo.ImageUrl = station.logo.URL;
- }
-
- list.Add(channelInfo);
- }
- }
-
- return list;
- }
-
- private ScheduleDirect.Station GetStation(List<ScheduleDirect.Station> allStations, string channelNumber, string channelName)
- {
- if (!string.IsNullOrWhiteSpace(channelName))
- {
- channelName = NormalizeName(channelName);
-
- var result = allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.callsign ?? string.Empty), channelName, StringComparison.OrdinalIgnoreCase));
+ Id = station.StationId,
+ CallSign = station.Callsign,
+ Number = channelNumber,
+ Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name
+ };
- if (result != null)
+ if (station.Logo != null)
{
- return result;
+ channelInfo.ImageUrl = station.Logo.Url;
}
- }
- if (!string.IsNullOrWhiteSpace(channelNumber))
- {
- return allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.stationID ?? string.Empty), channelNumber, StringComparison.OrdinalIgnoreCase));
+ list.Add(channelInfo);
}
- return null;
+ return list;
}
private static string NormalizeName(string value)
{
- return value.Replace(" ", string.Empty).Replace("-", string.Empty);
- }
-
- public class ScheduleDirect
- {
- public class Token
- {
- public int code { get; set; }
- public string message { get; set; }
- public string serverID { get; set; }
- public string token { get; set; }
- }
- public class Lineup
- {
- public string lineup { get; set; }
- public string name { get; set; }
- public string transport { get; set; }
- public string location { get; set; }
- public string uri { get; set; }
- }
-
- public class Lineups
- {
- public int code { get; set; }
- public string serverID { get; set; }
- public string datetime { get; set; }
- public List<Lineup> lineups { get; set; }
- }
-
-
- public class Headends
- {
- public string headend { get; set; }
- public string transport { get; set; }
- public string location { get; set; }
- public List<Lineup> lineups { get; set; }
- }
-
-
-
- public class Map
- {
- public string stationID { get; set; }
- public string channel { get; set; }
- public string logicalChannelNumber { get; set; }
- public int uhfVhf { get; set; }
- public int atscMajor { get; set; }
- public int atscMinor { get; set; }
- }
-
- public class Broadcaster
- {
- public string city { get; set; }
- public string state { get; set; }
- public string postalcode { get; set; }
- public string country { get; set; }
- }
-
- public class Logo
- {
- public string URL { get; set; }
- public int height { get; set; }
- public int width { get; set; }
- public string md5 { get; set; }
- }
-
- public class Station
- {
- public string stationID { get; set; }
- public string name { get; set; }
- public string callsign { get; set; }
- public List<string> broadcastLanguage { get; set; }
- public List<string> descriptionLanguage { get; set; }
- public Broadcaster broadcaster { get; set; }
- public string affiliate { get; set; }
- public Logo logo { get; set; }
- public bool? isCommercialFree { get; set; }
- }
-
- public class Metadata
- {
- public string lineup { get; set; }
- public string modified { get; set; }
- public string transport { get; set; }
- }
-
- public class Channel
- {
- public List<Map> map { get; set; }
- public List<Station> stations { get; set; }
- public Metadata metadata { get; set; }
- }
-
- public class RequestScheduleForChannel
- {
- public string stationID { get; set; }
- public List<string> date { get; set; }
- }
-
-
-
-
- public class Rating
- {
- public string body { get; set; }
- public string code { get; set; }
- }
-
- public class Multipart
- {
- public int partNumber { get; set; }
- public int totalParts { get; set; }
- }
-
- public class Program
- {
- public string programID { get; set; }
- public string airDateTime { get; set; }
- public int duration { get; set; }
- public string md5 { get; set; }
- public List<string> audioProperties { get; set; }
- public List<string> videoProperties { get; set; }
- public List<Rating> ratings { get; set; }
- public bool? @new { get; set; }
- public Multipart multipart { get; set; }
- public string liveTapeDelay { get; set; }
- public bool premiere { get; set; }
- public bool repeat { get; set; }
- public string isPremiereOrFinale { get; set; }
- }
-
-
-
- public class MetadataSchedule
- {
- public string modified { get; set; }
- public string md5 { get; set; }
- public string startDate { get; set; }
- public string endDate { get; set; }
- public int days { get; set; }
- }
-
- public class Day
- {
- public string stationID { get; set; }
- public List<Program> programs { get; set; }
- public MetadataSchedule metadata { get; set; }
-
- public Day()
- {
- programs = new List<Program>();
- }
- }
-
- //
- public class Title
- {
- public string title120 { get; set; }
- }
-
- public class EventDetails
- {
- public string subType { get; set; }
- }
-
- public class Description100
- {
- public string descriptionLanguage { get; set; }
- public string description { get; set; }
- }
-
- public class Description1000
- {
- public string descriptionLanguage { get; set; }
- public string description { get; set; }
- }
-
- public class DescriptionsProgram
- {
- public List<Description100> description100 { get; set; }
- public List<Description1000> description1000 { get; set; }
- }
-
- public class Gracenote
- {
- public int season { get; set; }
- public int episode { get; set; }
- }
-
- public class MetadataPrograms
- {
- public Gracenote Gracenote { get; set; }
- }
-
- public class ContentRating
- {
- public string body { get; set; }
- public string code { get; set; }
- }
-
- public class Cast
- {
- public string billingOrder { get; set; }
- public string role { get; set; }
- public string nameId { get; set; }
- public string personId { get; set; }
- public string name { get; set; }
- public string characterName { get; set; }
- }
-
- public class Crew
- {
- public string billingOrder { get; set; }
- public string role { get; set; }
- public string nameId { get; set; }
- public string personId { get; set; }
- public string name { get; set; }
- }
-
- public class QualityRating
- {
- public string ratingsBody { get; set; }
- public string rating { get; set; }
- public string minRating { get; set; }
- public string maxRating { get; set; }
- public string increment { get; set; }
- }
-
- public class Movie
- {
- public string year { get; set; }
- public int duration { get; set; }
- public List<QualityRating> qualityRating { get; set; }
- }
-
- public class Recommendation
- {
- public string programID { get; set; }
- public string title120 { get; set; }
- }
-
- public class ProgramDetails
- {
- public string audience { get; set; }
- public string programID { get; set; }
- public List<Title> titles { get; set; }
- public EventDetails eventDetails { get; set; }
- public DescriptionsProgram descriptions { get; set; }
- public string originalAirDate { get; set; }
- public List<string> genres { get; set; }
- public string episodeTitle150 { get; set; }
- public List<MetadataPrograms> metadata { get; set; }
- public List<ContentRating> contentRating { get; set; }
- public List<Cast> cast { get; set; }
- public List<Crew> crew { get; set; }
- public string entityType { get; set; }
- public string showType { get; set; }
- public bool hasImageArtwork { get; set; }
- public string primaryImage { get; set; }
- public string thumbImage { get; set; }
- public string backdropImage { get; set; }
- public string bannerImage { get; set; }
- public string imageID { get; set; }
- public string md5 { get; set; }
- public List<string> contentAdvisory { get; set; }
- public Movie movie { get; set; }
- public List<Recommendation> recommendations { get; set; }
- }
-
- public class Caption
- {
- public string content { get; set; }
- public string lang { get; set; }
- }
-
- public class ImageData
- {
- public string width { get; set; }
- public string height { get; set; }
- public string uri { get; set; }
- public string size { get; set; }
- public string aspect { get; set; }
- public string category { get; set; }
- public string text { get; set; }
- public string primary { get; set; }
- public string tier { get; set; }
- public Caption caption { get; set; }
- }
-
- public class ShowImages
- {
- public string programID { get; set; }
- public List<ImageData> data { get; set; }
- }
-
+ return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
new file mode 100644
index 000000000..95ac996e0
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Broadcaster dto.
+ /// </summary>
+ public class BroadcasterDto
+ {
+ /// <summary>
+ /// Gets or sets the city.
+ /// </summary>
+ [JsonPropertyName("city")]
+ public string? City { get; set; }
+
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ [JsonPropertyName("state")]
+ public string? State { get; set; }
+
+ /// <summary>
+ /// Gets or sets the postal code.
+ /// </summary>
+ [JsonPropertyName("postalCode")]
+ public string? Postalcode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country.
+ /// </summary>
+ [JsonPropertyName("country")]
+ public string? Country { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
new file mode 100644
index 000000000..f6251b9ad
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Caption dto.
+ /// </summary>
+ public class CaptionDto
+ {
+ /// <summary>
+ /// Gets or sets the content.
+ /// </summary>
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ /// <summary>
+ /// Gets or sets the lang.
+ /// </summary>
+ [JsonPropertyName("lang")]
+ public string? Lang { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
new file mode 100644
index 000000000..0b7a2c63a
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Cast dto.
+ /// </summary>
+ public class CastDto
+ {
+ /// <summary>
+ /// Gets or sets the billing order.
+ /// </summary>
+ [JsonPropertyName("billingOrder")]
+ public string? BillingOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ [JsonPropertyName("role")]
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name id.
+ /// </summary>
+ [JsonPropertyName("nameId")]
+ public string? NameId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the person id.
+ /// </summary>
+ [JsonPropertyName("personId")]
+ public string? PersonId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the character name.
+ /// </summary>
+ [JsonPropertyName("characterName")]
+ public string? CharacterName { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
new file mode 100644
index 000000000..87c327ed8
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Channel dto.
+ /// </summary>
+ public class ChannelDto
+ {
+ /// <summary>
+ /// Gets or sets the list of maps.
+ /// </summary>
+ [JsonPropertyName("map")]
+ public IReadOnlyList<MapDto> Map { get; set; } = Array.Empty<MapDto>();
+
+ /// <summary>
+ /// Gets or sets the list of stations.
+ /// </summary>
+ [JsonPropertyName("stations")]
+ public IReadOnlyList<StationDto> Stations { get; set; } = Array.Empty<StationDto>();
+
+ /// <summary>
+ /// Gets or sets the metadata.
+ /// </summary>
+ [JsonPropertyName("metadata")]
+ public MetadataDto? Metadata { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
new file mode 100644
index 000000000..c19cd2e48
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Content rating dto.
+ /// </summary>
+ public class ContentRatingDto
+ {
+ /// <summary>
+ /// Gets or sets the body.
+ /// </summary>
+ [JsonPropertyName("body")]
+ public string? Body { get; set; }
+
+ /// <summary>
+ /// Gets or sets the code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
new file mode 100644
index 000000000..f00c9accd
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Crew dto.
+ /// </summary>
+ public class CrewDto
+ {
+ /// <summary>
+ /// Gets or sets the billing order.
+ /// </summary>
+ [JsonPropertyName("billingOrder")]
+ public string? BillingOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ [JsonPropertyName("role")]
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name id.
+ /// </summary>
+ [JsonPropertyName("nameId")]
+ public string? NameId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the person id.
+ /// </summary>
+ [JsonPropertyName("personId")]
+ public string? PersonId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
new file mode 100644
index 000000000..1a371965c
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Day dto.
+ /// </summary>
+ public class DayDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of programs.
+ /// </summary>
+ [JsonPropertyName("programs")]
+ public IReadOnlyList<ProgramDto> Programs { get; set; } = Array.Empty<ProgramDto>();
+
+ /// <summary>
+ /// Gets or sets the metadata schedule.
+ /// </summary>
+ [JsonPropertyName("metadata")]
+ public MetadataScheduleDto? Metadata { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
new file mode 100644
index 000000000..ca6ae7fb1
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Description 1_000 dto.
+ /// </summary>
+ public class Description1000Dto
+ {
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public string? DescriptionLanguage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
new file mode 100644
index 000000000..1577219ed
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Description 100 dto.
+ /// </summary>
+ public class Description100Dto
+ {
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public string? DescriptionLanguage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
new file mode 100644
index 000000000..eaf4a340b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Descriptions program dto.
+ /// </summary>
+ public class DescriptionsProgramDto
+ {
+ /// <summary>
+ /// Gets or sets the list of description 100.
+ /// </summary>
+ [JsonPropertyName("description100")]
+ public IReadOnlyList<Description100Dto> Description100 { get; set; } = Array.Empty<Description100Dto>();
+
+ /// <summary>
+ /// Gets or sets the list of description1000.
+ /// </summary>
+ [JsonPropertyName("description1000")]
+ public IReadOnlyList<Description1000Dto> Description1000 { get; set; } = Array.Empty<Description1000Dto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
new file mode 100644
index 000000000..fbdfb1f71
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Event details dto.
+ /// </summary>
+ public class EventDetailsDto
+ {
+ /// <summary>
+ /// Gets or sets the sub type.
+ /// </summary>
+ [JsonPropertyName("subType")]
+ public string? SubType { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
new file mode 100644
index 000000000..6852d89d7
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Gracenote dto.
+ /// </summary>
+ public class GracenoteDto
+ {
+ /// <summary>
+ /// Gets or sets the season.
+ /// </summary>
+ [JsonPropertyName("season")]
+ public int Season { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode.
+ /// </summary>
+ [JsonPropertyName("episode")]
+ public int Episode { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
new file mode 100644
index 000000000..b9844562f
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Headends dto.
+ /// </summary>
+ public class HeadendsDto
+ {
+ /// <summary>
+ /// Gets or sets the headend.
+ /// </summary>
+ [JsonPropertyName("headend")]
+ public string? Headend { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transport.
+ /// </summary>
+ [JsonPropertyName("transport")]
+ public string? Transport { get; set; }
+
+ /// <summary>
+ /// Gets or sets the location.
+ /// </summary>
+ [JsonPropertyName("location")]
+ public string? Location { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of lineups.
+ /// </summary>
+ [JsonPropertyName("lineups")]
+ public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
new file mode 100644
index 000000000..a1ae3ca6d
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
@@ -0,0 +1,70 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Image data dto.
+ /// </summary>
+ public class ImageDataDto
+ {
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ [JsonPropertyName("width")]
+ public string? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ [JsonPropertyName("height")]
+ public string? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the uri.
+ /// </summary>
+ [JsonPropertyName("uri")]
+ public string? Uri { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ [JsonPropertyName("size")]
+ public string? Size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the aspect.
+ /// </summary>
+ [JsonPropertyName("aspect")]
+ public string? Aspect { get; set; }
+
+ /// <summary>
+ /// Gets or sets the category.
+ /// </summary>
+ [JsonPropertyName("category")]
+ public string? Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the text.
+ /// </summary>
+ [JsonPropertyName("text")]
+ public string? Text { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary.
+ /// </summary>
+ [JsonPropertyName("primary")]
+ public string? Primary { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tier.
+ /// </summary>
+ [JsonPropertyName("tier")]
+ public string? Tier { get; set; }
+
+ /// <summary>
+ /// Gets or sets the caption.
+ /// </summary>
+ [JsonPropertyName("caption")]
+ public CaptionDto? Caption { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
new file mode 100644
index 000000000..3dc64e5d8
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// The lineup dto.
+ /// </summary>
+ public class LineupDto
+ {
+ /// <summary>
+ /// Gets or sets the linup.
+ /// </summary>
+ [JsonPropertyName("lineup")]
+ public string? Lineup { get; set; }
+
+ /// <summary>
+ /// Gets or sets the lineup name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transport.
+ /// </summary>
+ [JsonPropertyName("transport")]
+ public string? Transport { get; set; }
+
+ /// <summary>
+ /// Gets or sets the location.
+ /// </summary>
+ [JsonPropertyName("location")]
+ public string? Location { get; set; }
+
+ /// <summary>
+ /// Gets or sets the uri.
+ /// </summary>
+ [JsonPropertyName("uri")]
+ public string? Uri { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this lineup was deleted.
+ /// </summary>
+ [JsonPropertyName("isDeleted")]
+ public bool? IsDeleted { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
new file mode 100644
index 000000000..f19081781
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Lineups dto.
+ /// </summary>
+ public class LineupsDto
+ {
+ /// <summary>
+ /// Gets or sets the response code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ [JsonPropertyName("serverID")]
+ public string? ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the datetime.
+ /// </summary>
+ [JsonPropertyName("datetime")]
+ public DateTime? LineupTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of lineups.
+ /// </summary>
+ [JsonPropertyName("lineups")]
+ public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
new file mode 100644
index 000000000..fecc55e03
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Logo dto.
+ /// </summary>
+ public class LogoDto
+ {
+ /// <summary>
+ /// Gets or sets the url.
+ /// </summary>
+ [JsonPropertyName("URL")]
+ public string? Url { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ [JsonPropertyName("height")]
+ public int Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ [JsonPropertyName("width")]
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
new file mode 100644
index 000000000..ffd02d474
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
@@ -0,0 +1,58 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Map dto.
+ /// </summary>
+ public class MapDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel.
+ /// </summary>
+ [JsonPropertyName("channel")]
+ public string? Channel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider callsign.
+ /// </summary>
+ [JsonPropertyName("providerCallsign")]
+ public string? ProvderCallsign { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logical channel number.
+ /// </summary>
+ [JsonPropertyName("logicalChannelNumber")]
+ public string? LogicalChannelNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the uhfvhf.
+ /// </summary>
+ [JsonPropertyName("uhfVhf")]
+ public int UhfVhf { get; set; }
+
+ /// <summary>
+ /// Gets or sets the atsc major.
+ /// </summary>
+ [JsonPropertyName("atscMajor")]
+ public int AtscMajor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the atsc minor.
+ /// </summary>
+ [JsonPropertyName("atscMinor")]
+ public int AtscMinor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the match type.
+ /// </summary>
+ [JsonPropertyName("matchType")]
+ public string? MatchType { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
new file mode 100644
index 000000000..40faa493c
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Metadata dto.
+ /// </summary>
+ public class MetadataDto
+ {
+ /// <summary>
+ /// Gets or sets the linup.
+ /// </summary>
+ [JsonPropertyName("lineup")]
+ public string? Lineup { get; set; }
+
+ /// <summary>
+ /// Gets or sets the modified timestamp.
+ /// </summary>
+ [JsonPropertyName("modified")]
+ public string? Modified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transport.
+ /// </summary>
+ [JsonPropertyName("transport")]
+ public string? Transport { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
new file mode 100644
index 000000000..43f290156
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Metadata programs dto.
+ /// </summary>
+ public class MetadataProgramsDto
+ {
+ /// <summary>
+ /// Gets or sets the gracenote object.
+ /// </summary>
+ [JsonPropertyName("Gracenote")]
+ public GracenoteDto? Gracenote { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
new file mode 100644
index 000000000..04560ab55
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Metadata schedule dto.
+ /// </summary>
+ public class MetadataScheduleDto
+ {
+ /// <summary>
+ /// Gets or sets the modified timestamp.
+ /// </summary>
+ [JsonPropertyName("modified")]
+ public string? Modified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start date.
+ /// </summary>
+ [JsonPropertyName("startDate")]
+ public DateTime? StartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end date.
+ /// </summary>
+ [JsonPropertyName("endDate")]
+ public DateTime? EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the days count.
+ /// </summary>
+ [JsonPropertyName("days")]
+ public int Days { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
new file mode 100644
index 000000000..31bef423b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Movie dto.
+ /// </summary>
+ public class MovieDto
+ {
+ /// <summary>
+ /// Gets or sets the year.
+ /// </summary>
+ [JsonPropertyName("year")]
+ public string? Year { get; set; }
+
+ /// <summary>
+ /// Gets or sets the duration.
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public int Duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of quality rating.
+ /// </summary>
+ [JsonPropertyName("qualityRating")]
+ public IReadOnlyList<QualityRatingDto> QualityRating { get; set; } = Array.Empty<QualityRatingDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
new file mode 100644
index 000000000..e8b15dc07
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Multipart dto.
+ /// </summary>
+ public class MultipartDto
+ {
+ /// <summary>
+ /// Gets or sets the part number.
+ /// </summary>
+ [JsonPropertyName("partNumber")]
+ public int PartNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total parts.
+ /// </summary>
+ [JsonPropertyName("totalParts")]
+ public int TotalParts { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
new file mode 100644
index 000000000..84c48f67f
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Program details dto.
+ /// </summary>
+ public class ProgramDetailsDto
+ {
+ /// <summary>
+ /// Gets or sets the audience.
+ /// </summary>
+ [JsonPropertyName("audience")]
+ public string? Audience { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of titles.
+ /// </summary>
+ [JsonPropertyName("titles")]
+ public IReadOnlyList<TitleDto> Titles { get; set; } = Array.Empty<TitleDto>();
+
+ /// <summary>
+ /// Gets or sets the event details object.
+ /// </summary>
+ [JsonPropertyName("eventDetails")]
+ public EventDetailsDto? EventDetails { get; set; }
+
+ /// <summary>
+ /// Gets or sets the descriptions.
+ /// </summary>
+ [JsonPropertyName("descriptions")]
+ public DescriptionsProgramDto? Descriptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the original air date.
+ /// </summary>
+ [JsonPropertyName("originalAirDate")]
+ public DateTime? OriginalAirDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of genres.
+ /// </summary>
+ [JsonPropertyName("genres")]
+ public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ [JsonPropertyName("episodeTitle150")]
+ public string? EpisodeTitle150 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of metadata.
+ /// </summary>
+ [JsonPropertyName("metadata")]
+ public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
+
+ /// <summary>
+ /// Gets or sets the list of content raitings.
+ /// </summary>
+ [JsonPropertyName("contentRating")]
+ public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
+
+ /// <summary>
+ /// Gets or sets the list of cast.
+ /// </summary>
+ [JsonPropertyName("cast")]
+ public IReadOnlyList<CastDto> Cast { get; set; } = Array.Empty<CastDto>();
+
+ /// <summary>
+ /// Gets or sets the list of crew.
+ /// </summary>
+ [JsonPropertyName("crew")]
+ public IReadOnlyList<CrewDto> Crew { get; set; } = Array.Empty<CrewDto>();
+
+ /// <summary>
+ /// Gets or sets the entity type.
+ /// </summary>
+ [JsonPropertyName("entityType")]
+ public string? EntityType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the show type.
+ /// </summary>
+ [JsonPropertyName("showType")]
+ public string? ShowType { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether there is image artwork.
+ /// </summary>
+ [JsonPropertyName("hasImageArtwork")]
+ public bool HasImageArtwork { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image.
+ /// </summary>
+ [JsonPropertyName("primaryImage")]
+ public string? PrimaryImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the thumb image.
+ /// </summary>
+ [JsonPropertyName("thumbImage")]
+ public string? ThumbImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backdrop image.
+ /// </summary>
+ [JsonPropertyName("backdropImage")]
+ public string? BackdropImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the banner image.
+ /// </summary>
+ [JsonPropertyName("bannerImage")]
+ public string? BannerImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image id.
+ /// </summary>
+ [JsonPropertyName("imageID")]
+ public string? ImageId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of content advisory.
+ /// </summary>
+ [JsonPropertyName("contentAdvisory")]
+ public IReadOnlyList<string> ContentAdvisory { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the movie object.
+ /// </summary>
+ [JsonPropertyName("movie")]
+ public MovieDto? Movie { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of recommendations.
+ /// </summary>
+ [JsonPropertyName("recommendations")]
+ public IReadOnlyList<RecommendationDto> Recommendations { get; set; } = Array.Empty<RecommendationDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
new file mode 100644
index 000000000..60389b45b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Program dto.
+ /// </summary>
+ public class ProgramDto
+ {
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the air date time.
+ /// </summary>
+ [JsonPropertyName("airDateTime")]
+ public DateTime? AirDateTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the duration.
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public int Duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of audio properties.
+ /// </summary>
+ [JsonPropertyName("audioProperties")]
+ public IReadOnlyList<string> AudioProperties { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the list of video properties.
+ /// </summary>
+ [JsonPropertyName("videoProperties")]
+ public IReadOnlyList<string> VideoProperties { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the list of ratings.
+ /// </summary>
+ [JsonPropertyName("ratings")]
+ public IReadOnlyList<RatingDto> Ratings { get; set; } = Array.Empty<RatingDto>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this program is new.
+ /// </summary>
+ [JsonPropertyName("new")]
+ public bool? New { get; set; }
+
+ /// <summary>
+ /// Gets or sets the multipart object.
+ /// </summary>
+ [JsonPropertyName("multipart")]
+ public MultipartDto? Multipart { get; set; }
+
+ /// <summary>
+ /// Gets or sets the live tape delay.
+ /// </summary>
+ [JsonPropertyName("liveTapeDelay")]
+ public string? LiveTapeDelay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this is the premiere.
+ /// </summary>
+ [JsonPropertyName("premiere")]
+ public bool Premiere { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this is a repeat.
+ /// </summary>
+ [JsonPropertyName("repeat")]
+ public bool Repeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the premiere or finale.
+ /// </summary>
+ [JsonPropertyName("isPremiereOrFinale")]
+ public string? IsPremiereOrFinale { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
new file mode 100644
index 000000000..c5ddcf7c5
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Quality rating dto.
+ /// </summary>
+ public class QualityRatingDto
+ {
+ /// <summary>
+ /// Gets or sets the ratings body.
+ /// </summary>
+ [JsonPropertyName("ratingsBody")]
+ public string? RatingsBody { get; set; }
+
+ /// <summary>
+ /// Gets or sets the rating.
+ /// </summary>
+ [JsonPropertyName("rating")]
+ public string? Rating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the min rating.
+ /// </summary>
+ [JsonPropertyName("minRating")]
+ public string? MinRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max rating.
+ /// </summary>
+ [JsonPropertyName("maxRating")]
+ public string? MaxRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the increment.
+ /// </summary>
+ [JsonPropertyName("increment")]
+ public string? Increment { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
new file mode 100644
index 000000000..e04b619a4
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Rating dto.
+ /// </summary>
+ public class RatingDto
+ {
+ /// <summary>
+ /// Gets or sets the body.
+ /// </summary>
+ [JsonPropertyName("body")]
+ public string? Body { get; set; }
+
+ /// <summary>
+ /// Gets or sets the code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
new file mode 100644
index 000000000..c8f79fd1c
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Recommendation dto.
+ /// </summary>
+ public class RecommendationDto
+ {
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ [JsonPropertyName("title120")]
+ public string? Title120 { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
new file mode 100644
index 000000000..0cd05709b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Request schedule for channel dto.
+ /// </summary>
+ public class RequestScheduleForChannelDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of dates.
+ /// </summary>
+ [JsonPropertyName("date")]
+ public IReadOnlyList<string> Date { get; set; } = Array.Empty<string>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
new file mode 100644
index 000000000..84e224b71
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Show image dto.
+ /// </summary>
+ public class ShowImagesDto
+ {
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of data.
+ /// </summary>
+ [JsonPropertyName("data")]
+ public IReadOnlyList<ImageDataDto> Data { get; set; } = Array.Empty<ImageDataDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
new file mode 100644
index 000000000..d797fd49b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Station dto.
+ /// </summary>
+ public class StationDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the callsign.
+ /// </summary>
+ [JsonPropertyName("callsign")]
+ public string? Callsign { get; set; }
+
+ /// <summary>
+ /// Gets or sets the broadcast language.
+ /// </summary>
+ [JsonPropertyName("broadcastLanguage")]
+ public IReadOnlyList<string> BroadcastLanguage { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public IReadOnlyList<string> DescriptionLanguage { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the broadcaster.
+ /// </summary>
+ [JsonPropertyName("broadcaster")]
+ public BroadcasterDto? Broadcaster { get; set; }
+
+ /// <summary>
+ /// Gets or sets the affiliate.
+ /// </summary>
+ [JsonPropertyName("affiliate")]
+ public string? Affiliate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logo.
+ /// </summary>
+ [JsonPropertyName("logo")]
+ public LogoDto? Logo { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether it is commercial free.
+ /// </summary>
+ [JsonPropertyName("isCommercialFree")]
+ public bool? IsCommercialFree { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
new file mode 100644
index 000000000..61cd4a9b0
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Title dto.
+ /// </summary>
+ public class TitleDto
+ {
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ [JsonPropertyName("title120")]
+ public string? Title120 { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
new file mode 100644
index 000000000..afb999486
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// The token dto.
+ /// </summary>
+ public class TokenDto
+ {
+ /// <summary>
+ /// Gets or sets the response code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response message.
+ /// </summary>
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ [JsonPropertyName("serverID")]
+ public string? ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the token.
+ /// </summary>
+ [JsonPropertyName("token")]
+ public string? Token { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current datetime.
+ /// </summary>
+ [JsonPropertyName("datetime")]
+ public DateTime? TokenTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response message.
+ /// </summary>
+ [JsonPropertyName("response")]
+ public string? Response { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 69b10e6da..0c0ec48d9 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -1,15 +1,20 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
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 Emby.XmlTv.Classes;
-using Emby.XmlTv.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.XmlTv;
+using Jellyfin.XmlTv.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
@@ -22,15 +27,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public class XmlTvListingsProvider : IListingsProvider
{
private readonly IServerConfigurationManager _config;
- private readonly IHttpClient _httpClient;
- private readonly ILogger _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger<XmlTvListingsProvider> _logger;
private readonly IFileSystem _fileSystem;
private readonly IZipClient _zipClient;
- public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IFileSystem fileSystem, IZipClient zipClient)
+ public XmlTvListingsProvider(
+ IServerConfigurationManager config,
+ IHttpClientFactory httpClientFactory,
+ ILogger<XmlTvListingsProvider> logger,
+ IFileSystem fileSystem,
+ IZipClient zipClient)
{
_config = config;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_fileSystem = fileSystem;
_zipClient = zipClient;
@@ -50,51 +60,41 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return _config.Configuration.PreferredMetadataLanguage;
}
- private async Task<string> GetXml(string path, CancellationToken cancellationToken)
+ private async Task<string> GetXml(ListingsProviderInfo info, CancellationToken cancellationToken)
{
- _logger.LogInformation("xmltv path: {path}", path);
+ _logger.LogInformation("xmltv path: {Path}", info.Path);
- if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- return UnzipIfNeeded(path, path);
+ return UnzipIfNeeded(info.Path, info.Path);
}
- string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml";
+ string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + "-" + info.Id + ".xml";
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
if (File.Exists(cacheFile))
{
- return UnzipIfNeeded(path, cacheFile);
+ return UnzipIfNeeded(info.Path, cacheFile);
}
- _logger.LogInformation("Downloading xmltv listings from {path}", path);
-
- string tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = path,
- Progress = new SimpleProgress<double>(),
- DecompressionMethod = CompressionMethod.Gzip,
-
- // It's going to come back gzipped regardless of this value
- // So we need to make sure the decompression method is set to gzip
- EnableHttpCompression = true,
-
- UserAgent = "Emby/3.0"
-
- }).ConfigureAwait(false);
+ _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
- File.Copy(tempFile, cacheFile, true);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
+ {
+ await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
+ }
- return UnzipIfNeeded(path, cacheFile);
+ return UnzipIfNeeded(info.Path, cacheFile);
}
- private string UnzipIfNeeded(string originalUrl, string file)
+ private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
{
- string ext = Path.GetExtension(originalUrl.Split('?')[0]);
+ ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
- if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
try
{
@@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error extracting from gz file {file}", file);
+ _logger.LogError(ex, "Error extracting from gz file {File}", file);
}
try
@@ -113,7 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error extracting from zip file {file}", file);
+ _logger.LogError(ex, "Error extracting from zip file {File}", file);
}
}
@@ -161,20 +161,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
throw new ArgumentNullException(nameof(channelId));
}
- /*
- if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false))
- {
- var length = endDateUtc - startDateUtc;
- if (length.TotalDays > 1)
- {
- endDateUtc = startDateUtc.AddDays(1);
- }
- }*/
+ _logger.LogDebug("Getting xmltv programs for channel {Id}", channelId);
- _logger.LogDebug("Getting xmltv programs for channel {id}", channelId);
-
- string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
- _logger.LogDebug("Opening XmlTvReader for {path}", path);
+ string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
+ _logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info));
return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken)
@@ -208,7 +198,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source),
OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null,
CommunityRating = program.StarRating,
- SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N")
+ SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
if (string.IsNullOrWhiteSpace(program.ProgramId))
@@ -219,19 +209,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture);
}
+
if (programInfo.EpisodeNumber.HasValue)
{
uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture);
}
- programInfo.ShowId = uniqueString.GetMD5().ToString("N");
+ programInfo.ShowId = uniqueString.GetMD5().ToString("N", CultureInfo.InvariantCulture);
// If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped
if (programInfo.IsSeries
&& !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
@@ -240,7 +231,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)
{
@@ -266,8 +257,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
{
// In theory this should never be called because there is always only one lineup
- string path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false);
- _logger.LogDebug("Opening XmlTvReader for {path}", path);
+ string path = await GetXml(info, CancellationToken.None).ConfigureAwait(false);
+ _logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info));
IEnumerable<XmlTvChannel> results = reader.GetChannels();
@@ -278,8 +269,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
{
// In theory this should never be called because there is always only one lineup
- string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
- _logger.LogDebug("Opening XmlTvReader for {path}", path);
+ string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
+ _logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info));
var results = reader.GetChannels();
@@ -290,7 +281,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/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
index f9b274acb..098f193fb 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
@@ -4,16 +4,20 @@ using MediaBrowser.Model.LiveTv;
namespace Emby.Server.Implementations.LiveTv
{
+ /// <summary>
+ /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
+ /// </summary>
public class LiveTvConfigurationFactory : IConfigurationFactory
{
+ /// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
- ConfigurationType = typeof(LiveTvOptions),
- Key = "livetv"
+ ConfigurationType = typeof(LiveTvOptions),
+ Key = "livetv"
}
};
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 1144c9ab1..21e1409ac 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -1,4 +1,9 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -19,9 +24,12 @@ namespace Emby.Server.Implementations.LiveTv
{
public class LiveTvDtoService
{
- private readonly ILogger _logger;
- private readonly IImageProcessor _imageProcessor;
+ private const string InternalVersionNumber = "4";
+
+ private const string ServiceName = "Emby";
+ private readonly ILogger<LiveTvDtoService> _logger;
+ private readonly IImageProcessor _imageProcessor;
private readonly IDtoService _dtoService;
private readonly IApplicationHost _appHost;
private readonly ILibraryManager _libraryManager;
@@ -29,13 +37,13 @@ namespace Emby.Server.Implementations.LiveTv
public LiveTvDtoService(
IDtoService dtoService,
IImageProcessor imageProcessor,
- ILoggerFactory loggerFactory,
+ ILogger<LiveTvDtoService> logger,
IApplicationHost appHost,
ILibraryManager libraryManager)
{
_dtoService = dtoService;
_imageProcessor = imageProcessor;
- _logger = loggerFactory.CreateLogger(nameof(LiveTvDtoService));
+ _logger = logger;
_appHost = appHost;
_libraryManager = libraryManager;
}
@@ -52,7 +60,7 @@ namespace Emby.Server.Implementations.LiveTv
ExternalId = info.Id,
ChannelId = GetInternalChannelId(service.Name, info.ChannelId),
Status = info.Status,
- SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N"),
+ SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture),
PrePaddingSeconds = info.PrePaddingSeconds,
PostPaddingSeconds = info.PostPaddingSeconds,
IsPostPaddingRequired = info.IsPostPaddingRequired,
@@ -69,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrEmpty(info.ProgramId))
{
- dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N");
+ dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture);
}
if (program != null)
@@ -107,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var dto = new SeriesTimerInfoDto
{
- Id = GetInternalSeriesTimerId(info.Id).ToString("N"),
+ Id = GetInternalSeriesTimerId(info.Id).ToString("N", CultureInfo.InvariantCulture),
Overview = info.Overview,
EndDate = info.EndDate,
Name = info.Name,
@@ -139,7 +147,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrEmpty(info.ProgramId))
{
- dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N");
+ dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture);
}
dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days.ToArray());
@@ -153,12 +161,11 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
DtoOptions = new DtoOptions(false)
-
}).FirstOrDefault();
if (librarySeries != null)
@@ -169,13 +176,14 @@ namespace Emby.Server.Implementations.LiveTv
try
{
dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image);
- dto.ParentThumbItemId = librarySeries.Id.ToString("N");
+ dto.ParentThumbItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error");
}
}
+
image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
if (image != null)
{
@@ -185,7 +193,7 @@ namespace Emby.Server.Implementations.LiveTv
{
_imageProcessor.GetImageCacheTag(librarySeries, image)
};
- dto.ParentBackdropItemId = librarySeries.Id.ToString("N");
+ dto.ParentBackdropItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
@@ -196,13 +204,12 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
DtoOptions = new DtoOptions(false),
Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
-
}).FirstOrDefault();
if (program != null)
@@ -213,7 +220,7 @@ namespace Emby.Server.Implementations.LiveTv
try
{
dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image);
- dto.ParentPrimaryImageItemId = program.Id.ToString("N");
+ dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
@@ -229,10 +236,11 @@ namespace Emby.Server.Implementations.LiveTv
try
{
dto.ParentBackdropImageTags = new string[]
- {
+ {
_imageProcessor.GetImageCacheTag(program, image)
- };
- dto.ParentBackdropItemId = program.Id.ToString("N");
+ };
+
+ dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
@@ -247,12 +255,11 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
DtoOptions = new DtoOptions(false)
-
}).FirstOrDefault();
if (librarySeries != null)
@@ -263,13 +270,14 @@ namespace Emby.Server.Implementations.LiveTv
try
{
dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image);
- dto.ParentThumbItemId = librarySeries.Id.ToString("N");
+ dto.ParentThumbItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error");
}
}
+
image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
if (image != null)
{
@@ -279,7 +287,7 @@ namespace Emby.Server.Implementations.LiveTv
{
_imageProcessor.GetImageCacheTag(librarySeries, image)
};
- dto.ParentBackdropItemId = librarySeries.Id.ToString("N");
+ dto.ParentBackdropItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
@@ -290,25 +298,23 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(Series).Name },
+ IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
-
}).FirstOrDefault();
if (program == null)
{
program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
DtoOptions = new DtoOptions(false),
Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
-
}).FirstOrDefault();
}
@@ -320,7 +326,7 @@ namespace Emby.Server.Implementations.LiveTv
try
{
dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image);
- dto.ParentPrimaryImageItemId = program.Id.ToString("N");
+ dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
@@ -339,7 +345,7 @@ namespace Emby.Server.Implementations.LiveTv
{
_imageProcessor.GetImageCacheTag(program, image)
};
- dto.ParentBackdropItemId = program.Id.ToString("N");
+ dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
@@ -393,8 +399,6 @@ namespace Emby.Server.Implementations.LiveTv
return null;
}
- private const string InternalVersionNumber = "4";
-
public Guid GetInternalChannelId(string serviceName, string externalId)
{
var name = serviceName + externalId + InternalVersionNumber;
@@ -402,12 +406,11 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel));
}
- private const string ServiceName = "Emby";
public string GetInternalTimerId(string externalId)
{
var name = ServiceName + externalId + InternalVersionNumber;
- return name.ToLowerInvariant().GetMD5().ToString("N");
+ return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
public Guid GetInternalSeriesTimerId(string externalId)
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index f7ef16fb0..ea1a28fe8 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,20 +1,25 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
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;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -22,96 +27,102 @@ 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;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace Emby.Server.Implementations.LiveTv
{
/// <summary>
- /// Class LiveTvManager
+ /// Class LiveTvManager.
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
+ private const int MaxGuideDays = 14;
+ private const string ExternalServiceTag = "ExternalServiceId";
+
+ private const string EtagKey = "ProgramEtag";
+
private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
+ private readonly ILogger<LiveTvManager> _logger;
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly Func<IChannelManager> _channelManager;
-
- private readonly IDtoService _dtoService;
private readonly ILocalizationManager _localization;
-
+ private readonly IFileSystem _fileSystem;
+ private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
private ILiveTvService[] _services = Array.Empty<ILiveTvService>();
-
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
- private readonly IFileSystem _fileSystem;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
- public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
-
- public string GetEmbyTvActiveRecordingPath(string id)
- {
- return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
- }
+ private bool _disposed = false;
public LiveTvManager(
- IServerApplicationHost appHost,
IServerConfigurationManager config,
- ILoggerFactory loggerFactory,
+ ILogger<LiveTvManager> logger,
IItemRepository itemRepo,
- IImageProcessor imageProcessor,
IUserDataManager userDataManager,
IDtoService dtoService,
IUserManager userManager,
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
- Func<IChannelManager> channelManager)
+ IChannelManager channelManager,
+ LiveTvDtoService liveTvDtoService)
{
_config = config;
- _logger = loggerFactory.CreateLogger(nameof(LiveTvManager));
+ _logger = logger;
_itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
- _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
_channelManager = channelManager;
-
- _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager);
+ _tvDtoService = liveTvDtoService;
}
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+
/// <summary>
/// Gets the services.
/// </summary>
/// <value>The services.</value>
public IReadOnlyList<ILiveTvService> Services => _services;
+ public ITunerHost[] TunerHosts => _tunerHosts;
+
+ public IListingsProvider[] ListingProviders => _listingProviders;
+
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
+ public string GetEmbyTvActiveRecordingPath(string id)
+ {
+ return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
+ }
+
/// <summary>
/// Adds the parts.
/// </summary>
@@ -129,51 +140,36 @@ namespace Emby.Server.Implementations.LiveTv
{
if (service is EmbyTV.EmbyTV embyTv)
{
- embyTv.TimerCreated += EmbyTv_TimerCreated;
- embyTv.TimerCancelled += EmbyTv_TimerCancelled;
+ embyTv.TimerCreated += OnEmbyTvTimerCreated;
+ embyTv.TimerCancelled += OnEmbyTvTimerCancelled;
}
}
}
- private void EmbyTv_TimerCancelled(object sender, GenericEventArgs<string> e)
+ private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs<string> e)
{
var timerId = e.Argument;
- TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
- {
- Id = timerId
- }
- });
+ TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(timerId)));
}
- private void EmbyTv_TimerCreated(object sender, GenericEventArgs<TimerInfo> e)
+ private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
- var service = sender as ILiveTvService;
- TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+ new TimerEventInfo(timer.Id)
{
- ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId),
- Id = timer.Id
- }
- });
+ ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId)
+ }));
}
- public ITunerHost[] TunerHosts => _tunerHosts;
-
- public IListingsProvider[] ListingProviders => _listingProviders;
-
public List<NameIdPair> GetTunerHostTypes()
{
return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
{
Name = i.Name,
Id = i.Type
-
}).ToList();
}
@@ -195,7 +191,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids,
IsSports = query.IsSports,
IsSeries = query.IsSeries,
- IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvChannel) },
TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked,
@@ -206,16 +202,16 @@ namespace Emby.Server.Implementations.LiveTv
var orderBy = internalQuery.OrderBy.ToList();
- orderBy.AddRange(query.SortBy.Select(i => new ValueTuple<string, SortOrder>(i, query.SortOrder ?? SortOrder.Ascending)));
+ orderBy.AddRange(query.SortBy.Select(i => (i, query.SortOrder ?? SortOrder.Ascending)));
if (query.EnableFavoriteSorting)
{
- orderBy.Insert(0, new ValueTuple<string, SortOrder>(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
+ orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
{
- orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending));
+ orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
internalQuery.OrderBy = orderBy.ToArray();
@@ -256,9 +252,10 @@ namespace Emby.Server.Implementations.LiveTv
var endTime = DateTime.UtcNow;
_logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
}
+
info.RequiresClosing = true;
- var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
+ var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_";
info.LiveStreamId = idPrefix + info.Id;
@@ -301,9 +298,12 @@ namespace Emby.Server.Implementations.LiveTv
}
private ILiveTvService GetService(string name)
- {
- return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
- }
+ => Array.Find(_services, x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
+ ?? throw new KeyNotFoundException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "No service with the name '{0}' can be found.",
+ name));
private static void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
{
@@ -354,30 +354,37 @@ namespace Emby.Server.Implementations.LiveTv
{
stream.BitRate = null;
}
+
if (stream.Channels.HasValue && stream.Channels <= 0)
{
stream.Channels = null;
}
+
if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
{
stream.AverageFrameRate = null;
}
+
if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
{
stream.RealFrameRate = null;
}
+
if (stream.Width.HasValue && stream.Width <= 0)
{
stream.Width = null;
}
+
if (stream.Height.HasValue && stream.Height <= 0)
{
stream.Height = null;
}
+
if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
{
stream.SampleRate = null;
}
+
if (stream.Level.HasValue && stream.Level <= 0)
{
stream.Level = null;
@@ -398,11 +405,11 @@ namespace Emby.Server.Implementations.LiveTv
// Set the total bitrate if not already supplied
mediaSource.InferTotalBitrate();
- if (!(service is EmbyTV.EmbyTV))
+ if (service is not EmbyTV.EmbyTV)
{
// We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
- //mediaSource.SupportsDirectPlay = false;
- //mediaSource.SupportsDirectStream = false;
+ // mediaSource.SupportsDirectPlay = false;
+ // mediaSource.SupportsDirectStream = false;
mediaSource.SupportsTranscoding = true;
foreach (var stream in mediaSource.MediaStreams)
{
@@ -419,8 +426,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private const string ExternalServiceTag = "ExternalServiceId";
- 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;
@@ -448,6 +454,7 @@ namespace Emby.Server.Implementations.LiveTv
{
isNew = true;
}
+
item.Tags = channelInfo.Tags;
}
@@ -455,6 +462,7 @@ namespace Emby.Server.Implementations.LiveTv
{
isNew = true;
}
+
item.ParentId = parentFolderId;
item.ChannelType = channelInfo.ChannelType;
@@ -464,24 +472,28 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.SetProviderId(ExternalServiceTag, serviceName);
if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
{
forceUpdate = true;
}
+
item.ExternalId = channelInfo.Id;
if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
{
forceUpdate = true;
}
+
item.Number = channelInfo.Number;
if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
{
forceUpdate = true;
}
+
item.Name = channelInfo.Name;
if (!item.HasImage(ImageType.Primary))
@@ -504,15 +516,13 @@ 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;
}
- private const string EtagKey = "ProgramEtag";
-
- private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
+ private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel)
{
var id = _tvDtoService.GetInternalProgramId(info.Id);
@@ -548,13 +558,12 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
- item.ParentId = channel.Id;
- //item.ChannelType = channelType;
+ item.ParentId = channel.Id;
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;
@@ -567,6 +576,7 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.ExternalSeriesId = seriesId;
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
@@ -581,30 +591,37 @@ namespace Emby.Server.Implementations.LiveTv
{
tags.Add("Live");
}
+
if (info.IsPremiere)
{
tags.Add("Premiere");
}
+
if (info.IsNews)
{
tags.Add("News");
}
+
if (info.IsSports)
{
tags.Add("Sports");
}
+
if (info.IsKids)
{
tags.Add("Kids");
}
+
if (info.IsRepeat)
{
tags.Add("Repeat");
}
+
if (info.IsMovie)
{
tags.Add("Movie");
}
+
if (isSeries)
{
tags.Add("Series");
@@ -627,11 +644,12 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
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;
@@ -644,12 +662,14 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.StartDate = info.StartDate;
if (item.EndDate != info.EndDate)
{
forceUpdate = true;
}
+
item.EndDate = info.EndDate;
item.ProductionYear = info.ProductionYear;
@@ -666,19 +686,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);
}
}
@@ -686,12 +710,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);
}
}
@@ -699,12 +724,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);
}
}
@@ -712,12 +738,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);
}
}
@@ -745,7 +772,7 @@ namespace Emby.Server.Implementations.LiveTv
item.OnMetadataChanged();
}
- return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated);
+ return (item, isNew, isUpdated);
}
public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
@@ -754,7 +781,8 @@ namespace Emby.Server.Implementations.LiveTv
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
- var list = new List<Tuple<BaseItemDto, string, string>>() {
+ var list = new List<Tuple<BaseItemDto, string, string>>
+ {
new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId)
};
@@ -769,31 +797,20 @@ namespace Emby.Server.Implementations.LiveTv
var topFolder = GetInternalLiveTvFolder(cancellationToken);
- if (query.OrderBy.Length == 0)
+ if (query.OrderBy.Count == 0)
{
- if (query.IsAiring ?? false)
- {
- // Unless something else was specified, order by start date to take advantage of a specialized index
- query.OrderBy = new ValueTuple<string, SortOrder>[]
- {
- new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending)
- };
- }
- else
+ // Unless something else was specified, order by start date to take advantage of a specialized index
+ query.OrderBy = new[]
{
- // Unless something else was specified, order by start date to take advantage of a specialized index
- query.OrderBy = new ValueTuple<string, SortOrder>[]
- {
- new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending)
- };
- }
+ (ItemSortBy.StartDate, SortOrder.Ascending)
+ };
}
RemoveFields(options);
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate,
@@ -819,8 +836,8 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
{
- var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
- var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
+ 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)
{
internalQuery.ExternalSeriesId = seriesTimer.SeriesId;
@@ -842,13 +859,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)
@@ -859,7 +874,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
IsAiring = query.IsAiring,
HasAired = query.HasAired,
IsNews = query.IsNews,
@@ -868,7 +883,7 @@ namespace Emby.Server.Implementations.LiveTv
IsSports = query.IsSports,
IsKids = query.IsKids,
EnableTotalRecordCount = query.EnableTotalRecordCount,
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) },
+ OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) },
TopParentIds = new[] { topFolder.Id },
DtoOptions = options,
GenreIds = query.GenreIds
@@ -880,7 +895,7 @@ namespace Emby.Server.Implementations.LiveTv
}
var programList = _libraryManager.QueryItems(internalQuery).Items;
- var totalCount = programList.Length;
+ var totalCount = programList.Count;
var orderedPrograms = programList.Cast<LiveTvProgram>().OrderBy(i => i.StartDate.Date);
@@ -965,11 +980,8 @@ namespace Emby.Server.Implementations.LiveTv
private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken)
{
- var timers = new Dictionary<string, List<TimerInfo>>();
- var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>();
-
- TimerInfo[] timerList = null;
- SeriesTimerInfo[] seriesTimerList = null;
+ IReadOnlyList<TimerInfo> timerList = null;
+ IReadOnlyList<SeriesTimerInfo> seriesTimerList = null;
foreach (var programTuple in programs)
{
@@ -977,10 +989,7 @@ namespace Emby.Server.Implementations.LiveTv
var externalProgramId = programTuple.Item2;
string externalSeriesId = programTuple.Item3;
- if (timerList == null)
- {
- timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
- }
+ timerList ??= (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
var foundSeriesTimer = false;
@@ -997,7 +1006,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrEmpty(timer.SeriesTimerId))
{
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.SeriesTimerId)
- .ToString("N");
+ .ToString("N", CultureInfo.InvariantCulture);
foundSeriesTimer = true;
}
@@ -1008,17 +1017,14 @@ namespace Emby.Server.Implementations.LiveTv
continue;
}
- if (seriesTimerList == null)
- {
- seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
- }
+ seriesTimerList ??= (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
if (seriesTimer != null)
{
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id)
- .ToString("N");
+ .ToString("N", CultureInfo.InvariantCulture);
}
}
}
@@ -1079,16 +1085,16 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase)
{
- CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
- CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
+ CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
+ CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
}
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
if (coreService != null)
{
- await coreService.RefreshSeriesTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false);
- await coreService.RefreshTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false);
+ await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
+ await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
}
// Load these now which will prefetch metadata
@@ -1119,7 +1125,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);
}
@@ -1136,7 +1142,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);
@@ -1171,11 +1177,9 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
-
- IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
-
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<LiveTvProgram>();
@@ -1183,14 +1187,14 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var program in channelPrograms)
{
- var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
- var programItem = programTuple.Item1;
+ var programTuple = GetProgram(program, existingPrograms, currentChannel);
+ var programItem = programTuple.item;
- if (programTuple.Item2)
+ if (programTuple.isNew)
{
newPrograms.Add(programItem);
}
- else if (programTuple.Item3)
+ else if (programTuple.isUpdated)
{
updatedPrograms.Add(programItem);
}
@@ -1213,7 +1217,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;
@@ -1226,12 +1234,13 @@ namespace Emby.Server.Implementations.LiveTv
currentChannel.AddTag("Kids");
}
- //currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
- await currentChannel.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
- {
- ForceSave = true
-
- }, cancellationToken).ConfigureAwait(false);
+ await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ await currentChannel.RefreshMetadata(
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -1245,7 +1254,7 @@ namespace Emby.Server.Implementations.LiveTv
numComplete++;
double percent = numComplete / (double)allChannelsList.Count;
- progress.Report(85 * percent + 15);
+ progress.Report((85 * percent) + 15);
}
progress.Report(100);
@@ -1278,12 +1287,14 @@ namespace Emby.Server.Implementations.LiveTv
if (item != null)
{
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false,
- DeleteFromExternalProvider = false
-
- }, false);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false,
+ DeleteFromExternalProvider = false
+ },
+ false);
}
}
@@ -1294,7 +1305,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private const int MaxGuideDays = 14;
private double GetGuideDays()
{
var config = GetConfiguration();
@@ -1332,28 +1342,31 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsMovie.Value)
{
- includeItemTypes.Add(typeof(Movie).Name);
+ includeItemTypes.Add(nameof(Movie));
}
else
{
- excludeItemTypes.Add(typeof(Movie).Name);
+ excludeItemTypes.Add(nameof(Movie));
}
}
+
if (query.IsSeries.HasValue)
{
if (query.IsSeries.Value)
{
- includeItemTypes.Add(typeof(Episode).Name);
+ includeItemTypes.Add(nameof(Episode));
}
else
{
- excludeItemTypes.Add(typeof(Episode).Name);
+ excludeItemTypes.Add(nameof(Episode));
}
}
+
if (query.IsSports ?? false)
{
genres.Add("Sports");
}
+
if (query.IsKids ?? false)
{
genres.Add("Kids");
@@ -1368,14 +1381,14 @@ namespace Emby.Server.Implementations.LiveTv
// limit = (query.Limit ?? 10) * 2;
limit = null;
- //var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray();
- //var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
+ // var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray();
+ // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
- //return new QueryResult<BaseItem>
- //{
+ // return new QueryResult<BaseItem>
+ // {
// Items = items,
// TotalRecordCount = items.Length
- //};
+ // };
dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray();
}
@@ -1389,7 +1402,7 @@ namespace Emby.Server.Implementations.LiveTv
IsVirtualItem = false,
Limit = limit,
StartIndex = query.StartIndex,
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
EnableTotalRecordCount = query.EnableTotalRecordCount,
IncludeItemTypes = includeItemTypes.ToArray(),
ExcludeItemTypes = excludeItemTypes.ToArray(),
@@ -1399,29 +1412,28 @@ namespace Emby.Server.Implementations.LiveTv
if (query.IsInProgress ?? false)
{
- //TODO Fix The co-variant conversion between Video[] and BaseItem[], this can generate runtime issues.
+ // TODO: Fix The co-variant conversion between Video[] and BaseItem[], this can generate runtime issues.
result.Items = result
.Items
.OfType<Video>()
.Where(i => !i.IsCompleteMedia)
.ToArray();
- result.TotalRecordCount = result.Items.Length;
+ result.TotalRecordCount = result.Items.Count;
}
return result;
}
- public Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, ItemFields[] fields, User user = null)
+ public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null)
{
var programTuples = new List<Tuple<BaseItemDto, string, string>>();
var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo);
- foreach (var tuple in tuples)
+ foreach (var (item, dto) in programs)
{
- var program = (LiveTvProgram)tuple.Item1;
- var dto = tuple.Item2;
+ var program = (LiveTvProgram)item;
dto.StartDate = program.StartDate;
dto.EpisodeTitle = program.EpisodeTitle;
@@ -1472,7 +1484,7 @@ namespace Emby.Server.Implementations.LiveTv
dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId)
? null
- : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N");
+ : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture);
dto.TimerId = string.IsNullOrEmpty(info.Id)
? null
@@ -1596,8 +1608,6 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrEmpty(query.Id))
{
- var guid = new Guid(query.Id);
-
timers = timers
.Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase));
}
@@ -1706,22 +1716,16 @@ 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);
await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
- if (!(service is EmbyTV.EmbyTV))
+ if (service is not EmbyTV.EmbyTV)
{
- TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
- {
- Id = id
- }
- });
+ TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
}
}
@@ -1731,29 +1735,24 @@ 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);
await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
- SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
- {
- Id = id
- }
- });
+ SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
}
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));
}
@@ -1801,11 +1800,7 @@ namespace Emby.Server.Implementations.LiveTv
}
var returnArray = timers
- .Select(i =>
- {
- return i.Item1;
-
- })
+ .Select(i => i.Item1)
.ToArray();
return new QueryResult<SeriesTimerInfo>
@@ -1859,7 +1854,6 @@ namespace Emby.Server.Implementations.LiveTv
}
return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName);
-
})
.ToArray();
@@ -1876,23 +1870,22 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetItemById(internalChannelId);
}
- public void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> tuples, DtoOptions options, User user)
+ public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user)
{
var now = DateTime.UtcNow;
- var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray();
+ var channelIds = items.Select(i => i.Item2.Id).Distinct().ToArray();
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ IncludeItemTypes = new[] { nameof(LiveTvProgram) },
ChannelIds = channelIds,
MaxStartDate = now,
MinEndDate = now,
Limit = channelIds.Length,
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) },
+ OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) },
TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id },
DtoOptions = options
-
}) : new List<BaseItem>();
RemoveFields(options);
@@ -1902,7 +1895,7 @@ namespace Emby.Server.Implementations.LiveTv
var addCurrentProgram = options.AddCurrentProgram;
- foreach (var tuple in tuples)
+ foreach (var tuple in items)
{
var dto = tuple.Item1;
var channel = tuple.Item2;
@@ -1930,7 +1923,7 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var programDto in currentProgramDtos)
{
- if (currentChannelsDict.TryGetValue(programDto.ChannelId, out BaseItemDto channelDto))
+ if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto))
{
channelDto.CurrentProgram = programDto;
}
@@ -1970,16 +1963,13 @@ namespace Emby.Server.Implementations.LiveTv
OriginalAirDate = program.PremiereDate,
Overview = program.Overview,
StartDate = program.StartDate,
- //ImagePath = program.ExternalImagePath,
+ // ImagePath = program.ExternalImagePath,
Name = program.Name,
OfficialRating = program.OfficialRating
};
}
- if (service == null)
- {
- service = _services.First();
- }
+ service ??= _services[0];
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -2004,9 +1994,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)
@@ -2022,12 +2010,12 @@ namespace Emby.Server.Implementations.LiveTv
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
info.Name = program.Name;
- info.ChannelId = programDto.ChannelId;
+ info.ChannelId = programDto.ChannelId ?? Guid.Empty;
info.ChannelName = programDto.ChannelName;
info.StartDate = program.StartDate;
info.Name = program.Name;
info.Overview = program.Overview;
- info.ProgramId = programDto.Id.ToString("N");
+ info.ProgramId = programDto.Id.ToString("N", CultureInfo.InvariantCulture);
info.ExternalProgramId = program.ExternalId;
if (program.EndDate.HasValue)
@@ -2061,16 +2049,13 @@ namespace Emby.Server.Implementations.LiveTv
_logger.LogInformation("New recording scheduled");
- if (!(service is EmbyTV.EmbyTV))
+ if (service is not EmbyTV.EmbyTV)
{
- TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+ new TimerEventInfo(newTimerId)
{
- ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId),
- Id = newTimerId
- }
- });
+ ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId)
+ }));
}
}
@@ -2088,21 +2073,18 @@ namespace Emby.Server.Implementations.LiveTv
if (service is ISupportsNewTimerIds supportsNewTimerIds)
{
newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false);
- newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N");
+ newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N", CultureInfo.InvariantCulture);
}
else
{
await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
}
- SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
+ SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+ new TimerEventInfo(newTimerId)
{
- ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId),
- Id = newTimerId
- }
- });
+ ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId)
+ }));
}
public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken)
@@ -2135,15 +2117,13 @@ namespace Emby.Server.Implementations.LiveTv
};
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
+ /// <inheritdoc />
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
- private bool _disposed = false;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -2187,20 +2167,19 @@ namespace Emby.Server.Implementations.LiveTv
var info = new LiveTvInfo
{
Services = services,
- IsEnabled = services.Length > 0
+ IsEnabled = services.Length > 0,
+ EnabledUsers = _userManager.Users
+ .Where(IsLiveTvEnabled)
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .ToArray()
};
- info.EnabledUsers = _userManager.Users
- .Where(IsLiveTvEnabled)
- .Select(i => i.Id.ToString("N"))
- .ToArray();
-
return info;
}
private bool IsLiveTvEnabled(User user)
{
- return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
+ return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
}
public IEnumerable<User> GetEnabledUsers()
@@ -2217,9 +2196,9 @@ namespace Emby.Server.Implementations.LiveTv
/// <returns>Task.</returns>
public Task ResetTuner(string id, CancellationToken cancellationToken)
{
- var parts = id.Split(new[] { '_' }, 2);
+ var parts = id.Split('_', 2);
- var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
+ var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase));
if (service == null)
{
@@ -2248,7 +2227,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.SerializeToUtf8Bytes(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2269,7 +2248,7 @@ namespace Emby.Server.Implementations.LiveTv
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
{
- info.Id = Guid.NewGuid().ToString("N");
+ info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
list.Add(info);
config.TunerHosts = list.ToArray();
}
@@ -2282,7 +2261,7 @@ namespace Emby.Server.Implementations.LiveTv
if (dataSourceChanged)
{
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
return info;
@@ -2292,15 +2271,17 @@ 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.SerializeToUtf8Bytes(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
if (provider == null)
{
throw new ResourceNotFoundException(
- string.Format("Couldn't find provider of type: '{0}'", info.Type)
- );
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Couldn't find provider of type: '{0}'",
+ info.Type));
}
await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
@@ -2312,7 +2293,7 @@ namespace Emby.Server.Implementations.LiveTv
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
{
- info.Id = Guid.NewGuid().ToString("N");
+ info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
list.Add(info);
config.ListingProviders = list.ToArray();
}
@@ -2323,7 +2304,7 @@ namespace Emby.Server.Implementations.LiveTv
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
@@ -2335,23 +2316,23 @@ namespace Emby.Server.Implementations.LiveTv
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
- public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
+ public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{
var config = GetConfiguration();
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
- listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray();
+ listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
- if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{
var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair
{
- Name = tunerChannelId,
- Value = providerChannelId
+ Name = tunerChannelNumber,
+ Value = providerChannelNumber
});
listingsProviderInfo.ChannelMappings = list.ToArray();
}
@@ -2369,12 +2350,12 @@ namespace Emby.Server.Implementations.LiveTv
var tunerChannelMappings =
tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
- return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
+ return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
}
- public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> epgChannels)
+ public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
{
var result = new TunerChannelMapping
{
@@ -2387,7 +2368,7 @@ namespace Emby.Server.Implementations.LiveTv
result.Name = tunerChannel.Number + " " + result.Name;
}
- var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
+ var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
if (providerChannel != null)
{
@@ -2462,20 +2443,18 @@ 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())
.OrderBy(i => i.SortName)
.ToList();
- folders.AddRange(_channelManager().GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+ folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
{
UserId = user.Id,
IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels
-
}).Items);
return folders.Cast<BaseItem>().ToList();
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index cd1731de5..ecd28097d 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -1,63 +1,56 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
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 StreamIdDelimiter = '_';
+
private readonly ILiveTvManager _liveTvManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly ILogger _logger;
+ private readonly ILogger<LiveTvMediaSourceProvider> _logger;
private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationHost _appHost;
- private IApplicationPaths _appPaths;
- public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
+ public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
{
_liveTvManager = liveTvManager;
- _jsonSerializer = jsonSerializer;
+ _logger = logger;
_mediaSourceManager = mediaSourceManager;
- _mediaEncoder = mediaEncoder;
_appHost = appHost;
- _logger = loggerFactory.CreateLogger(GetType().Name);
- _appPaths = appPaths;
}
public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
{
- var baseItem = (BaseItem)item;
-
- if (baseItem.SourceType == SourceType.LiveTV)
+ if (item.SourceType == SourceType.LiveTV)
{
var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path);
- if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null)
+ if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null)
{
return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken);
}
}
- return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
+ return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
}
- // 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 async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
{
IEnumerable<MediaSourceInfo> sources;
@@ -85,12 +78,11 @@ namespace Emby.Server.Implementations.LiveTv
}
var list = sources.ToList();
- var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
foreach (var source in list)
{
source.Type = MediaSourceType.Default;
- source.BufferMs = source.BufferMs ?? 1500;
+ source.BufferMs ??= 1500;
if (source.RequiresOpening || forceRequireOpening)
{
@@ -99,28 +91,32 @@ namespace Emby.Server.Implementations.LiveTv
if (source.RequiresOpening)
{
- var openKeys = new List<string>();
- openKeys.Add(item.GetType().Name);
- openKeys.Add(item.Id.ToString("N"));
- openKeys.Add(source.Id ?? string.Empty);
- source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray());
+ var openKeys = new List<string>
+ {
+ item.GetType().Name,
+ item.Id.ToString("N", CultureInfo.InvariantCulture),
+ source.Id ?? string.Empty
+ };
+
+ source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
}
// Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
{
- source.Path = serverUrl;
+ source.Path = _appHost.GetSmartApiUrl(string.Empty);
}
}
- _logger.LogDebug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
+ _logger.LogDebug("MediaSources: {@MediaSources}", list);
return list;
}
+ /// <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/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
index 542951de4..15df0dcf1 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.LiveTv;
@@ -8,40 +9,61 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.LiveTv
{
- public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ /// <summary>
+ /// The "Refresh Guide" scheduled task.
+ /// </summary>
+ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILiveTvManager _liveTvManager;
private readonly IConfigurationManager _config;
- public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
+ /// </summary>
+ /// <param name="liveTvManager">The live tv manager.</param>
+ /// <param name="config">The configuration manager.</param>
+ public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
_config = config;
}
+ /// <inheritdoc />
public string Name => "Refresh Guide";
+ /// <inheritdoc />
public string Description => "Downloads channel information from live tv services.";
+ /// <inheritdoc />
public string Category => "Live TV";
- public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public string Key => "RefreshGuide";
+
+ /// <inheritdoc />
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var manager = (LiveTvManager)_liveTvManager;
return manager.RefreshChannels(progress, cancellationToken);
}
- /// <summary>
- /// Creates the triggers that define when the task will run
- /// </summary>
- /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ /// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return new[] {
-
+ return new[]
+ {
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
@@ -49,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
-
- public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
-
- public string Key => "RefreshGuide";
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 715f600a1..2b82f2462 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -1,8 +1,12 @@
+#nullable disable
+
+#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;
@@ -12,53 +16,52 @@ 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
{
public abstract class BaseTunerHost
{
- protected readonly IServerConfigurationManager Config;
- protected readonly ILogger 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 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;
}
+ protected IServerConfigurationManager Config { get; }
+
+ protected ILogger<BaseTunerHost> Logger { get; }
+
+ protected IFileSystem FileSystem { get; }
+
public virtual bool IsSupported => true;
- protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
public abstract string Type { get; }
+ protected virtual string ChannelIdPrefix => Type + "_";
+
+ protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
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();
- //logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
+ 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;
@@ -93,11 +96,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
- JsonSerializer.SerializeToFile(channels, channelCacheFile);
+ await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
+ await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
-
}
}
}
@@ -109,12 +112,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile);
+ await using var readStream = AsyncFile.OpenRead(channelCacheFile);
+ var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
list.AddRange(channels);
}
catch (IOException)
{
-
}
}
}
@@ -158,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return new List<MediaSourceInfo>();
}
- protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
+ protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
@@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException();
}
- protected virtual string ChannelIdPrefix => Type + "_";
-
protected virtual bool IsValidChannelId(string channelId)
{
if (string.IsNullOrEmpty(channelId))
@@ -233,10 +235,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/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
new file mode 100644
index 000000000..0f0453189
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
@@ -0,0 +1,23 @@
+#nullable disable
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ internal class Channels
+ {
+ public string GuideNumber { get; set; }
+
+ public string GuideName { get; set; }
+
+ public string VideoCodec { get; set; }
+
+ public string AudioCodec { get; set; }
+
+ public string URL { get; set; }
+
+ public bool Favorite { get; set; }
+
+ public bool DRM { get; set; }
+
+ public bool HD { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
new file mode 100644
index 000000000..42068cd34
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
@@ -0,0 +1,42 @@
+#nullable disable
+
+using System;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ internal class DiscoverResponse
+ {
+ public string FriendlyName { get; set; }
+
+ public string ModelNumber { get; set; }
+
+ public string FirmwareName { get; set; }
+
+ public string FirmwareVersion { get; set; }
+
+ public string DeviceID { get; set; }
+
+ public string DeviceAuth { get; set; }
+
+ public string BaseURL { get; set; }
+
+ public string LineupURL { get; set; }
+
+ public int TunerCount { get; set; }
+
+ public bool SupportsTranscoding
+ {
+ get
+ {
+ var model = ModelNumber ?? string.Empty;
+
+ if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
new file mode 100644
index 000000000..069b4fab6
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
@@ -0,0 +1,35 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class HdHomerunChannelCommands : IHdHomerunChannelCommands
+ {
+ private string? _channel;
+ private string? _profile;
+
+ public HdHomerunChannelCommands(string? channel, string? profile)
+ {
+ _channel = channel;
+ _profile = profile;
+ }
+
+ public IEnumerable<(string, string)> GetCommands()
+ {
+ if (!string.IsNullOrEmpty(_channel))
+ {
+ if (!string.IsNullOrEmpty(_profile)
+ && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
+ {
+ yield return ("vchannel", $"{_channel} transcode={_profile}");
+ }
+ else
+ {
+ yield return ("vchannel", _channel);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 24b100edd..78ea7bd0f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -1,123 +1,113 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
+using System.Globalization;
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;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.System;
+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 JsonSerializerOptions _jsonOptions;
+
+ private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
public HdHomerunHost(
IServerConfigurationManager config,
- ILogger logger,
- IJsonSerializer jsonSerializer,
+ ILogger<HdHomerunHost> logger,
IFileSystem fileSystem,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- INetworkManager networkManager)
- : base(config, logger, jsonSerializer, fileSystem)
+ IStreamHelper streamHelper,
+ IMemoryCache memoryCache)
+ : base(config, logger, fileSystem, memoryCache)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
- _networkManager = networkManager;
+ _streamHelper = streamHelper;
+
+ _jsonOptions = JsonDefaults.Options;
}
public string Name => "HD Homerun";
- public override string Type => DeviceType;
-
- public static string DeviceType => "hdhomerun";
+ public override string Type => "hdhomerun";
protected override string ChannelIdPrefix => "hdhr_";
- private string GetChannelId(TunerHostInfo info, Channels i)
- {
- var id = ChannelIdPrefix + i.GuideNumber;
+ private string GetChannelId(Channels i)
+ => ChannelIdPrefix + i.GuideNumber;
- return id;
- }
-
- private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
+ internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- var options = new HttpRequestOptions
- {
- Url = model.LineupURL,
- CancellationToken = cancellationToken,
- BufferContent = false
- };
- using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
- {
- using (var stream = response.Content)
- {
- var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
-
- if (info.ImportFavoritesOnly)
- {
- lineup = lineup.Where(i => i.Favorite).ToList();
- }
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
+ .ConfigureAwait(false) ?? new List<Channels>();
- return lineup.Where(i => !i.DRM).ToList();
- }
+ if (info.ImportFavoritesOnly)
+ {
+ lineup = lineup.Where(i => i.Favorite).ToList();
}
- }
- private class HdHomerunChannelInfo : ChannelInfo
- {
- public bool IsLegacyTuner { get; set; }
+ return lineup.Where(i => !i.DRM).ToList();
}
- protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
- var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
+ var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
return lineup.Select(i => new HdHomerunChannelInfo
{
Name = i.GuideName,
Number = i.GuideNumber,
- Id = GetChannelId(info, i),
+ Id = GetChannelId(i),
IsFavorite = i.Favorite,
- TunerHostId = info.Id,
- IsHD = i.HD == 1,
+ TunerHostId = tuner.Id,
+ IsHD = i.HD,
AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec,
ChannelType = ChannelType.TV,
IsLegacyTuner = (i.URL ?? string.Empty).StartsWith("hdhomerun", StringComparison.OrdinalIgnoreCase),
Path = i.URL
-
}).Cast<ChannelInfo>().ToList();
}
- private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
- private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
+ internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{
var cacheKey = info.Id;
@@ -134,39 +124,32 @@ 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,
- TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(10).TotalMilliseconds),
- BufferContent = false
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
+ .ConfigureAwait(false);
- }, "GET").ConfigureAwait(false))
+ if (!string.IsNullOrEmpty(cacheKey))
{
- using (var stream = response.Content)
+ lock (_modelCache)
{
- var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
-
- if (!string.IsNullOrEmpty(cacheKey))
- {
- lock (_modelCache)
- {
- _modelCache[cacheKey] = discoverResponse;
- }
- }
-
- return discoverResponse;
+ _modelCache[cacheKey] = discoverResponse;
}
}
+
+ return discoverResponse;
}
- catch (HttpException ex)
+ catch (HttpRequestException 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))
{
@@ -176,6 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_modelCache[cacheKey] = response;
}
}
+
return response;
}
@@ -187,43 +171,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- using (var stream = await _httpClient.Get(new HttpRequestOptions()
- {
- Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
- CancellationToken = cancellationToken,
- TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
- BufferContent = false
- }))
+ 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(cancellationToken).ConfigureAwait(false);
+ using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+ var tuners = new List<LiveTvTunerInfo>();
+ await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
- var tuners = new List<LiveTvTunerInfo>();
- using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
+ string stripedLine = StripXML(line);
+ if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
- while (!sr.EndOfStream)
+ LiveTvTunerStatus status;
+ var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = stripedLine.Substring(0, index - 1);
+ var currentChannel = stripedLine.Substring(index + 7);
+ if (string.Equals(currentChannel, "none", 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") { 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;
}
+
+ tuners.Add(new LiveTvTunerInfo
+ {
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+ ProgramName = currentChannel,
+ Status = status
+ });
}
- return tuners;
}
+
+ 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;
@@ -236,17 +227,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
inside = true;
continue;
}
+
if (let == '>')
{
inside = false;
continue;
}
+
if (!inside)
{
- buffer[bufferIndex] = let;
- bufferIndex++;
+ buffer[bufferIndex++] = let;
}
}
+
return new string(buffer, 0, bufferIndex);
}
@@ -254,19 +247,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- var tuners = new List<LiveTvTunerInfo>();
+ var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
var uri = new Uri(GetApiUrl(info));
- using (var manager = new HdHomerunManager(_socketFactory, Logger))
+ using (var manager = new HdHomerunManager())
{
// Legacy HdHomeruns are IPv4 only
- var ipInfo = _networkManager.ParseIpAddress(uri.Host);
+ var ipInfo = IPAddress.Parse(uri.Host);
- for (int i = 0; i < model.TunerCount; ++i)
+ for (int i = 0; i < model.TunerCount; i++)
{
- var name = string.Format("Tuner {0}", i + 1);
- var currentChannel = "none"; /// @todo Get current channel and map back to Station Id
+ 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;
tuners.Add(new LiveTvTunerInfo
@@ -278,6 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
});
}
}
+
return tuners;
}
@@ -304,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
{
// TODO Need faster way to determine UDP vs HTTP
- var channels = await GetChannels(info, true, cancellationToken);
+ var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false);
var hdHomerunChannelInfo = channels.FirstOrDefault() as HdHomerunChannelInfo;
@@ -333,23 +327,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/');
}
- private class Channels
- {
- public string GuideNumber { get; set; }
- public string GuideName { get; set; }
- public string VideoCodec { get; set; }
- public string AudioCodec { get; set; }
- public string URL { get; set; }
- public bool Favorite { get; set; }
- public bool DRM { get; set; }
- public int HD { get; set; }
- }
-
- protected EncodingOptions GetEncodingOptions()
- {
- return Config.GetConfiguration<EncodingOptions>("encoding");
- }
-
private static string GetHdHrIdFromChannelId(string channelId)
{
return channelId.Split('_')[1];
@@ -436,12 +413,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
videoCodec = channelInfo.VideoCodec;
}
+
string audioCodec = channelInfo.AudioCodec;
- if (!videoBitrate.HasValue)
- {
- videoBitrate = isHd ? 15000000 : 2000000;
- }
+ videoBitrate ??= isHd ? 15000000 : 2000000;
+
int? audioBitrate = isHd ? 448000 : 192000;
// normalize
@@ -463,36 +439,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
id = "native";
}
- id += "_" + channelId.GetMD5().ToString("N") + "_" + url.GetMD5().ToString("N");
+
+ id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
var mediaSource = new MediaSourceInfo
{
Path = url,
Protocol = MediaProtocol.Udp,
MediaStreams = new List<MediaStream>
- {
- new MediaStream
- {
- Type = MediaStreamType.Video,
- // Set the index to -1 because we don't know the exact index of the video stream within the container
- Index = -1,
- IsInterlaced = isInterlaced,
- Codec = videoCodec,
- Width = width,
- Height = height,
- BitRate = videoBitrate,
- NalLengthSize = nal
-
- },
- new MediaStream
- {
- Type = MediaStreamType.Audio,
- // Set the index to -1 because we don't know the exact index of the audio stream within the container
- Index = -1,
- Codec = audioCodec,
- BitRate = audioBitrate
- }
- },
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = isInterlaced,
+ Codec = videoCodec,
+ Width = width,
+ Height = height,
+ BitRate = videoBitrate,
+ NalLengthSize = nal
+ },
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ // Set the index to -1 because we don't know the exact index of the audio stream within the container
+ Index = -1,
+ Codec = audioCodec,
+ BitRate = audioBitrate
+ }
+ },
RequiresOpening = true,
RequiresClosing = true,
BufferMs = 0,
@@ -503,8 +479,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
SupportsTranscoding = true,
IsInfiniteStream = true,
IgnoreDts = true,
- //IgnoreIndex = true,
- //ReadAtNativeFramerate = true
+ // IgnoreIndex = true,
+ // ReadAtNativeFramerate = true
};
mediaSource.InferTotalBitrate();
@@ -512,79 +488,91 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return mediaSource;
}
- protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
+ protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{
var list = new List<MediaSourceInfo>();
- var channelId = channelInfo.Id;
+ var channelId = channel.Id;
var hdhrId = GetHdHrIdFromChannelId(channelId);
- var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
-
- var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
-
- if (isLegacyTuner)
+ if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner)
{
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
else
{
- try
- {
- var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+ var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false);
- if (modelInfo != null && modelInfo.SupportsTranscoding)
+ if (modelInfo != null && modelInfo.SupportsTranscoding)
+ {
+ if (tuner.AllowHWTranscoding)
{
- if (info.AllowHWTranscoding)
- {
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
-
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
- }
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile"));
}
- }
- catch
- {
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
if (list.Count == 0)
{
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
}
return list;
}
- protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
+ protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- var profile = streamId.Split('_')[0];
+ var tunerCount = tunerHost.TunerCount;
+
+ if (tunerCount > 0)
+ {
+ var tunerHostId = tunerHost.Id;
+ var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
+
+ if (liveStreams.Count() >= tunerCount)
+ {
+ throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached.");
+ }
+ }
+
+ var profile = streamId.AsSpan().LeftPart('_').ToString();
- Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile);
+ Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
- var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id);
+ var hdhrId = GetHdHrIdFromChannelId(channel.Id);
- var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
+ var hdhomerunChannel = channel as HdHomerunChannelInfo;
- var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+ var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false);
if (!modelInfo.SupportsTranscoding)
{
profile = "native";
}
- var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
+ var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile);
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
{
- return new HdHomerunUdpStream(mediaSource, info, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
+ return new HdHomerunUdpStream(
+ mediaSource,
+ tunerHost,
+ streamId,
+ new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
+ modelInfo.TunerCount,
+ FileSystem,
+ Logger,
+ Config,
+ _appHost,
+ _streamHelper);
}
var enableHttpStream = true;
@@ -592,19 +580,39 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
mediaSource.Protocol = MediaProtocol.Http;
- var httpUrl = channelInfo.Path;
+ var httpUrl = channel.Path;
// If raw was used, the tuner doesn't support params
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
{
httpUrl += "?transcode=" + profile;
}
- mediaSource.Path = httpUrl;
- return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
- }
+ mediaSource.Path = httpUrl;
- return new HdHomerunUdpStream(mediaSource, info, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
+ return new SharedHttpStream(
+ mediaSource,
+ tunerHost,
+ streamId,
+ FileSystem,
+ _httpClientFactory,
+ Logger,
+ Config,
+ _appHost,
+ _streamHelper);
+ }
+
+ return new HdHomerunUdpStream(
+ mediaSource,
+ tunerHost,
+ streamId,
+ new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
+ modelInfo.TunerCount,
+ FileSystem,
+ Logger,
+ Config,
+ _appHost,
+ _streamHelper);
}
public async Task Validate(TunerHostInfo info)
@@ -620,7 +628,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
info.DeviceId = modelInfo.DeviceID;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
{
@@ -632,34 +640,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- public class DiscoverResponse
- {
- public string FriendlyName { get; set; }
- public string ModelNumber { get; set; }
- public string FirmwareName { get; set; }
- public string FirmwareVersion { get; set; }
- public string DeviceID { get; set; }
- public string DeviceAuth { get; set; }
- public string BaseURL { get; set; }
- public string LineupURL { get; set; }
- public int TunerCount { get; set; }
-
- public bool SupportsTranscoding
- {
- get
- {
- var model = ModelNumber ?? string.Empty;
-
- if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
- {
- return true;
- }
-
- return false;
- }
- }
- }
-
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
{
lock (_modelCache)
@@ -667,7 +647,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_modelCache.Clear();
}
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
+ using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs);
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationToken.Token, cancellationToken);
+ cancellationToken = linkedCancellationTokenSource.Token;
var list = new List<TunerHostInfo>();
// Create udp broadcast discovery message
@@ -677,13 +659,13 @@ 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 IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 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)
{
var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
- var deviceIp = response.RemoteEndPoint.IpAddress.Address;
+ var deviceIp = response.RemoteEndPoint.Address.ToString();
// check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
@@ -698,21 +680,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
}
-
}
catch (OperationCanceledException)
{
}
- catch
+ catch (Exception ex)
{
// Socket timeout indicates all messages have been received.
+ Logger.LogError(ex, "Error while sending discovery message");
}
}
return list;
}
- private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
+ internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
{
var hostInfo = new TunerHostInfo
{
@@ -720,21 +702,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Url = url
};
- try
- {
- var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
+ var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
- hostInfo.DeviceId = modelInfo.DeviceID;
- hostInfo.FriendlyName = modelInfo.FriendlyName;
+ hostInfo.DeviceId = modelInfo.DeviceID;
+ hostInfo.FriendlyName = modelInfo.FriendlyName;
+ hostInfo.TunerCount = modelInfo.TunerCount;
- return hostInfo;
- }
- catch
- {
- // logged at lower levels
- }
+ return hostInfo;
+ }
- return null;
+ private class HdHomerunChannelInfo : ChannelInfo
+ {
+ public bool IsLegacyTuner { get; set; }
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 2205c0ecc..f9d151a81 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -1,203 +1,150 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
-using System.Collections.Generic;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Globalization;
using System.Net;
+using System.Net.Sockets;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
- public interface IHdHomerunChannelCommands
- {
- IEnumerable<Tuple<string, string>> GetCommands();
- }
-
- public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
- {
- private string _channel;
- private string _program;
- public LegacyHdHomerunChannelCommands(string url)
- {
- // parse url for channel and program
- var regExp = new Regex(@"\/ch(\d+)-?(\d*)");
- var match = regExp.Match(url);
- if (match.Success)
- {
- _channel = match.Groups[1].Value;
- _program = match.Groups[2].Value;
- }
- }
-
- public IEnumerable<Tuple<string, string>> GetCommands()
- {
- var commands = new List<Tuple<string, string>>();
-
- if (!string.IsNullOrEmpty(_channel))
- commands.Add(Tuple.Create("channel", _channel));
-
- if (!string.IsNullOrEmpty(_program))
- commands.Add(Tuple.Create("program", _program));
- return commands;
- }
- }
-
- public class HdHomerunChannelCommands : IHdHomerunChannelCommands
+ public sealed class HdHomerunManager : IDisposable
{
- private string _channel;
- private string _profile;
+ public const int HdHomeRunPort = 65001;
- public HdHomerunChannelCommands(string channel, string profile)
- {
- _channel = channel;
- _profile = profile;
- }
-
- public IEnumerable<Tuple<string, string>> GetCommands()
- {
- var commands = new List<Tuple<string, string>>();
-
- if (!string.IsNullOrEmpty(_channel))
- {
- if (!string.IsNullOrEmpty(_profile) && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
- {
- commands.Add(Tuple.Create("vchannel", string.Format("{0} transcode={1}", _channel, _profile)));
- }
- else
- {
- commands.Add(Tuple.Create("vchannel", _channel));
- }
- }
-
- return commands;
- }
- }
-
- public class HdHomerunManager : IDisposable
- {
- public static int HdHomeRunPort = 65001;
// Message constants
- private static byte GetSetName = 3;
- private static byte GetSetValue = 4;
- private static byte GetSetLockkey = 21;
- private static ushort GetSetRequest = 4;
- private static ushort GetSetReply = 5;
+ private const byte GetSetName = 3;
+ private const byte GetSetValue = 4;
+ private const byte GetSetLockkey = 21;
+ private const ushort GetSetRequest = 4;
+ private const ushort GetSetReply = 5;
private uint? _lockkey = null;
private int _activeTuner = -1;
- private readonly ISocketFactory _socketFactory;
- private IpAddressInfo _remoteIp;
+ private IPEndPoint _remoteEndPoint;
- private ILogger _logger;
- private ISocket _currentTcpSocket;
-
- public HdHomerunManager(ISocketFactory socketFactory, ILogger logger)
- {
- _socketFactory = socketFactory;
- _logger = logger;
- }
+ private TcpClient _tcpClient;
public void Dispose()
{
- using (var socket = _currentTcpSocket)
+ using (var socket = _tcpClient)
{
if (socket != null)
{
- _currentTcpSocket = null;
+ _tcpClient = null;
- var task = StopStreaming(socket);
- Task.WaitAll(task);
+ StopStreaming(socket).GetAwaiter().GetResult();
}
}
- }
- public async Task<bool> CheckTunerAvailability(IpAddressInfo remoteIp, int tuner, CancellationToken cancellationToken)
- {
- using (var socket = _socketFactory.CreateTcpSocket(remoteIp, HdHomeRunPort))
- {
- return await CheckTunerAvailability(socket, remoteIp, tuner, cancellationToken).ConfigureAwait(false);
- }
+ GC.SuppressFinalize(this);
}
- private static async Task<bool> CheckTunerAvailability(ISocket socket, IpAddressInfo remoteIp, int tuner, CancellationToken cancellationToken)
+ public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
{
- var ipEndPoint = new IpEndPointInfo(remoteIp, HdHomeRunPort);
+ using var client = new TcpClient();
+ client.Connect(remoteIp, HdHomeRunPort);
- var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
- await socket.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken);
+ using var stream = client.GetStream();
+ return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
+ }
- var receiveBuffer = new byte[8192];
- var response = await socket.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
+ private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
+ {
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
+ try
+ {
+ var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
+ await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
- ParseReturnMessage(response.Buffer, response.ReceivedBytes, out string returnVal);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase);
+ return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
- public async Task StartStreaming(IpAddressInfo remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
+ public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
{
- _remoteIp = remoteIp;
-
- var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort);
- _currentTcpSocket = tcpClient;
+ _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
- var receiveBuffer = new byte[8192];
+ _tcpClient = new TcpClient();
+ _tcpClient.Connect(_remoteEndPoint);
if (!_lockkey.HasValue)
{
- var rand = new Random();
- _lockkey = (uint)rand.Next();
+ _lockkey = (uint)Random.Shared.Next();
}
var lockKeyValue = _lockkey.Value;
+ var stream = _tcpClient.GetStream();
- var ipEndPoint = new IpEndPointInfo(_remoteIp, HdHomeRunPort);
-
- for (int i = 0; i < numTuners; ++i)
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
+ try
{
- if (!await CheckTunerAvailability(tcpClient, _remoteIp, i, cancellationToken).ConfigureAwait(false))
- continue;
-
- _activeTuner = i;
- var lockKeyString = string.Format("{0:d}", lockKeyValue);
- var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
- await tcpClient.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false);
- var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
- // parse response to make sure it worked
- if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out var returnVal))
- continue;
-
- var commandList = commands.GetCommands();
- foreach (Tuple<string, string> command in commandList)
+ for (int i = 0; i < numTuners; ++i)
{
- var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
- await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false);
- response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
+ if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false))
+ {
+ continue;
+ }
+
+ _activeTuner = i;
+ var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
+ var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
+ await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+
// parse response to make sure it worked
- if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
- await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
}
- }
+ foreach (var command in commands.GetCommands())
+ {
+ var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
+ await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+ receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+
+ // parse response to make sure it worked
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
+ {
+ await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
+ continue;
+ }
+ }
- var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
- var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
+ var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
+ var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
- await tcpClient.SendToAsync(targetMsg, 0, targetMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false);
- response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
- // parse response to make sure it worked
- if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal))
- {
- await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false);
- continue;
- }
+ await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
+ receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- return;
+ // parse response to make sure it worked
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
+ {
+ await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
+ continue;
+ }
+
+ return;
+ }
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
}
_activeTuner = -1;
@@ -207,286 +154,201 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken)
{
if (!_lockkey.HasValue)
+ {
return;
+ }
- using (var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort))
- {
- var commandList = commands.GetCommands();
- var receiveBuffer = new byte[8192];
+ using var tcpClient = new TcpClient();
+ tcpClient.Connect(_remoteEndPoint);
- foreach (Tuple<string, string> command in commandList)
+ using var stream = tcpClient.GetStream();
+ var commandList = commands.GetCommands();
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
+ try
+ {
+ foreach (var command in commandList)
{
- var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
- await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, new IpEndPointInfo(_remoteIp, HdHomeRunPort), cancellationToken).ConfigureAwait(false);
- var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
+ var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
+ await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+
// parse response to make sure it worked
- if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out string returnVal))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
return;
}
}
}
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
- public Task StopStreaming(ISocket socket)
+ public Task StopStreaming(TcpClient client)
{
var lockKey = _lockkey;
if (!lockKey.HasValue)
+ {
return Task.CompletedTask;
+ }
- return ReleaseLockkey(socket, lockKey.Value);
+ return ReleaseLockkey(client, lockKey.Value);
}
- private async Task ReleaseLockkey(ISocket tcpClient, uint lockKeyValue)
+ private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue)
{
- _logger.LogInformation("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue);
+ var stream = client.GetStream();
- var ipEndPoint = new IpEndPointInfo(_remoteIp, HdHomeRunPort);
-
- var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
- await tcpClient.SendToAsync(releaseTarget, 0, releaseTarget.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false);
-
- var receiveBuffer = new byte[8192];
-
- await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false);
- var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
- _lockkey = null;
- await tcpClient.SendToAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false);
- await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false);
+ var buffer = ArrayPool<byte>.Shared.Rent(8192);
+ try
+ {
+ var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
+ await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
+
+ await stream.ReadAsync(buffer).ConfigureAwait(false);
+ var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
+ _lockkey = null;
+ await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
+ await stream.ReadAsync(buffer).ConfigureAwait(false);
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
- private static byte[] CreateGetMessage(int tuner, string name)
+ internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name));
- int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
-
- var message = new byte[messageLength];
-
- int offset = InsertHeaderAndName(byteName, messageLength, message);
-
- bool flipEndian = BitConverter.IsLittleEndian;
-
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
- Array.Reverse(crcBytes);
- Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
- return message;
+ var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+ int offset = WriteHeaderAndPayload(buffer, byteName);
+ return FinishPacket(buffer, offset);
}
- private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
+ internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name));
- var byteValue = Encoding.UTF8.GetBytes(string.Format("{0}\0", value));
-
- int messageLength = byteName.Length + byteValue.Length + 12;
- if (lockkey.HasValue)
- messageLength += 6;
-
- var message = new byte[messageLength];
-
- int offset = InsertHeaderAndName(byteName, messageLength, message);
+ var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+ int offset = WriteHeaderAndPayload(buffer, byteName);
- bool flipEndian = BitConverter.IsLittleEndian;
+ buffer[offset++] = GetSetValue;
+ offset += WriteNullTerminatedString(buffer.Slice(offset), value);
- message[offset] = GetSetValue;
- offset++;
- message[offset] = Convert.ToByte(byteValue.Length);
- offset++;
- Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
- offset += byteValue.Length;
if (lockkey.HasValue)
{
- message[offset] = GetSetLockkey;
- offset++;
- message[offset] = (byte)4;
- offset++;
- var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
- if (flipEndian)
- Array.Reverse(lockKeyBytes);
- Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
+ buffer[offset++] = GetSetLockkey;
+ buffer[offset++] = 4;
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
offset += 4;
}
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
- Array.Reverse(crcBytes);
- Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
- return message;
+ return FinishPacket(buffer, offset);
}
- private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message)
+ internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
{
- // check to see if we need to flip endiannes
- bool flipEndian = BitConverter.IsLittleEndian;
- int offset = 0;
+ int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
- // create header bytes
- var getSetBytes = BitConverter.GetBytes(GetSetRequest);
- var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc
+ // TODO: variable length: this can be 2 bytes if len > 127
+ // Write length in front of value
+ buffer[0] = Convert.ToByte(len);
- if (flipEndian)
- {
- Array.Reverse(getSetBytes);
- Array.Reverse(msgLenBytes);
- }
+ // null-terminate
+ buffer[len++] = 0;
+
+ return len;
+ }
+
+ private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
+ {
+ // Packet type
+ BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
- // insert header bytes into message
- Buffer.BlockCopy(getSetBytes, 0, message, offset, 2);
- offset += 2;
- Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2);
- offset += 2;
+ // We write the payload length at the end
+ int offset = 4;
- // insert tag name and length
- message[offset] = GetSetName;
- offset++;
- message[offset] = Convert.ToByte(byteName.Length);
- offset++;
+ // Tag
+ buffer[offset++] = GetSetName;
- // insert name string
- Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
- offset += byteName.Length;
+ // Payload length + data
+ int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
+ offset += strLen;
return offset;
}
- private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
+ private static int FinishPacket(Span<byte> buffer, int offset)
{
- returnVal = string.Empty;
+ // Payload length
+ BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
- if (numBytes < 4)
- return false;
+ // calculate crc and insert at the end of the message
+ var crc = Crc32.Compute(buffer.Slice(0, offset));
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
- var flipEndian = BitConverter.IsLittleEndian;
- int offset = 0;
- byte[] msgTypeBytes = new byte[2];
- Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
+ return offset + 4;
+ }
- if (flipEndian)
- Array.Reverse(msgTypeBytes);
+ internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected)
+ {
+ return TryGetReturnValueOfGetSet(buffer, out var value)
+ && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
+ }
- var msgType = BitConverter.ToUInt16(msgTypeBytes, 0);
- offset += 2;
+ internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value)
+ {
+ value = ReadOnlySpan<byte>.Empty;
- if (msgType != GetSetReply)
+ if (buffer.Length < 8)
+ {
return false;
+ }
- byte[] msgLengthBytes = new byte[2];
- Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length);
- if (flipEndian)
- Array.Reverse(msgLengthBytes);
+ uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
+ if (crc != Crc32.Compute(buffer[..^4]))
+ {
+ return false;
+ }
- var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0);
- offset += 2;
+ if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
+ {
+ return false;
+ }
- if (numBytes < msgLength + 8)
+ var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
+ if (buffer.Length != 2 + 2 + 4 + msgLength)
+ {
return false;
+ }
- var nameTag = buf[offset];
- offset++;
+ var offset = 4;
+ if (buffer[offset++] != GetSetName)
+ {
+ return false;
+ }
- var nameLength = buf[offset];
- offset++;
+ var nameLength = buffer[offset++];
+ if (buffer.Length < 4 + 1 + offset + nameLength)
+ {
+ return false;
+ }
- // skip the name field to get to value for return
offset += nameLength;
- var valueTag = buf[offset];
- offset++;
-
- var valueLength = buf[offset];
- offset++;
-
- returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
- return true;
- }
+ if (buffer[offset++] != GetSetValue)
+ {
+ return false;
+ }
- private class HdHomerunCrc
- {
- private static uint[] crc_table = {
- 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
- 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
- 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
- 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
- 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
- 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
- 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
- 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
- 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
- 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
- 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
- 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
- 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
- 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
- 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
- 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
- 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
- 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
- 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
- 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
- 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
- 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
- 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
- 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
- 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
- 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
- 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
- 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
- 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
- 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
- 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
- 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
- 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
- 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
- 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
- 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
- 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
- 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
- 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
- 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
- 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
- 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
- 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
- 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
- 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
- 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
- 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
- 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
- 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
- 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
- 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
- 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
- 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
- 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
- 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
- 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
- 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
- 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
- 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
- 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
- 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
- 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
- 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
- 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
-
- public static uint GetCrc32(byte[] bytes, int numBytes)
+ var valueLength = buffer[offset++];
+ if (buffer.Length < 4 + offset + valueLength)
{
- var hash = 0xffffffff;
- for (var i = 0; i < numBytes; i++)
- hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
-
- var tmp = ~hash & 0xffffffff;
- var b0 = tmp & 0xff;
- var b1 = (tmp >> 8) & 0xff;
- var b2 = (tmp >> 16) & 0xff;
- var b3 = (tmp >> 24) & 0xff;
- hash = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
- return hash;
+ return false;
}
+
+ // remove null terminator
+ value = buffer.Slice(offset, valueLength - 1);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 8774371d5..31445e1ec 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -1,11 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net;
+using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -18,30 +24,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider
{
- private readonly IServerApplicationHost _appHost;
- private readonly MediaBrowser.Model.Net.ISocketFactory _socketFactory;
+ private const int RtpHeaderBytes = 12;
+ private readonly IServerApplicationHost _appHost;
private readonly IHdHomerunChannelCommands _channelCommands;
private readonly int _numTuners;
- private readonly INetworkManager _networkManager;
- public HdHomerunUdpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IHdHomerunChannelCommands channelCommands, int numTuners, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, MediaBrowser.Model.Net.ISocketFactory socketFactory, INetworkManager networkManager)
- : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths)
+ public HdHomerunUdpStream(
+ MediaSourceInfo mediaSource,
+ TunerHostInfo tunerHostInfo,
+ string originalStreamId,
+ IHdHomerunChannelCommands channelCommands,
+ int numTuners,
+ IFileSystem fileSystem,
+ ILogger logger,
+ IConfigurationManager configurationManager,
+ IServerApplicationHost appHost,
+ IStreamHelper streamHelper)
+ : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_appHost = appHost;
- _socketFactory = socketFactory;
- _networkManager = networkManager;
OriginalStreamId = originalStreamId;
_channelCommands = channelCommands;
_numTuners = numTuners;
EnableStreamSharing = true;
}
- private static Socket CreateSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
+ /// <summary>
+ /// Returns an unused UDP port number in the range specified.
+ /// Temporarily placed here until future network PR merged.
+ /// </summary>
+ /// <param name="range">Upper and Lower boundary of ports to select.</param>
+ /// <returns>System.Int32.</returns>
+ private static int GetUdpPortFromRange((int Min, int Max) range)
{
- var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp);
+ var properties = IPGlobalProperties.GetIPGlobalProperties();
+
+ // Get active udp listeners.
+ var udpListenerPorts = properties.GetActiveUdpListeners()
+ .Where(n => n.Port >= range.Min && n.Port <= range.Max)
+ .Select(n => n.Port);
- return socket;
+ return Enumerable
+ .Range(range.Min, range.Max)
+ .FirstOrDefault(i => !udpListenerPorts.Contains(i));
}
public override async Task Open(CancellationToken openCancellationToken)
@@ -51,22 +77,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var mediaSource = OriginalMediaSource;
var uri = new Uri(mediaSource.Path);
- var localPort = _networkManager.GetRandomUnusedUdpPort();
+ // Temporary code to reduce PR size. This will be updated by a future network pr.
+ var localPort = GetUdpPortFromRange((49152, 65535));
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host);
var remoteAddress = IPAddress.Parse(uri.Host);
- var embyRemoteAddress = _networkManager.ParseIpAddress(uri.Host);
IPAddress localAddress = null;
- using (var tcpSocket = CreateSocket(remoteAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
+ using (var tcpClient = new TcpClient())
{
try
{
- tcpSocket.Connect(new IPEndPoint(remoteAddress, HdHomerunManager.HdHomeRunPort));
- localAddress = ((IPEndPoint)tcpSocket.LocalEndPoint).Address;
- tcpSocket.Close();
+ await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false);
+ localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
+ tcpClient.Close();
}
catch (Exception ex)
{
@@ -75,237 +101,129 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- var udpClient = _socketFactory.CreateUdpSocket(localPort);
- var hdHomerunManager = new HdHomerunManager(_socketFactory, Logger);
+ if (localAddress.IsIPv4MappedToIPv6)
+ {
+ localAddress = localAddress.MapToIPv4();
+ }
+
+ var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork);
+ var hdHomerunManager = new HdHomerunManager();
try
{
// send url to start streaming
- await hdHomerunManager.StartStreaming(embyRemoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false);
+ await hdHomerunManager.StartStreaming(
+ remoteAddress,
+ localAddress,
+ localPort,
+ _channelCommands,
+ _numTuners,
+ openCancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
using (udpClient)
using (hdHomerunManager)
{
- if (!(ex is OperationCanceledException))
+ if (ex is not OperationCanceledException)
{
Logger.LogError(ex, "Error opening live stream:");
}
+
throw;
}
}
var taskCompletionSource = new TaskCompletionSource<bool>();
- await StartStreaming(udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
+ _ = StartStreaming(
+ udpClient,
+ hdHomerunManager,
+ remoteAddress,
+ taskCompletionSource,
+ LiveStreamCancellationTokenSource.Token);
- //OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.Path = tempFile;
- //OpenedMediaSource.ReadAtNativeFramerate = true;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.Path = tempFile;
+ // OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
- //OpenedMediaSource.SupportsDirectPlay = false;
- //OpenedMediaSource.SupportsDirectStream = true;
- //OpenedMediaSource.SupportsTranscoding = true;
+ // OpenedMediaSource.SupportsDirectPlay = false;
+ // OpenedMediaSource.SupportsDirectStream = true;
+ // OpenedMediaSource.SupportsTranscoding = true;
- //await Task.Delay(5000).ConfigureAwait(false);
+ // await Task.Delay(5000).ConfigureAwait(false);
await taskCompletionSource.Task.ConfigureAwait(false);
}
- private Task StartStreaming(MediaBrowser.Model.Net.ISocket udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
+ private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
- return Task.Run(async () =>
+ using (udpClient)
+ using (hdHomerunManager)
{
- using (udpClient)
- using (hdHomerunManager)
+ try
{
- try
- {
- await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException ex)
- {
- Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
- openTaskCompletionSource.TrySetException(ex);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error opening live stream:");
- openTaskCompletionSource.TrySetException(ex);
- }
-
- EnableStreamSharing = false;
+ await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error opening live stream:");
+ openTaskCompletionSource.TrySetException(ex);
}
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
- });
- }
+ EnableStreamSharing = false;
+ }
- private static void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
- {
- Task.Run(() =>
- {
- openTaskCompletionSource.TrySetResult(true);
- });
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
}
- private static int RtpHeaderBytes = 12;
- private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
+ private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
- var bufferSize = 81920;
-
- byte[] buffer = new byte[bufferSize];
- int read;
var resolved = false;
- using (var source = _socketFactory.CreateNetworkStream(udpClient, false))
- using (var fileStream = FileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
{
- var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token;
-
- while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0)
+ while (true)
{
cancellationToken.ThrowIfCancellationRequested();
-
- currentCancellationToken = cancellationToken;
-
- read -= RtpHeaderBytes;
-
- if (read > 0)
+ using (var timeOutSource = new CancellationTokenSource())
+ using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken,
+ timeOutSource.Token))
{
- fileStream.Write(buffer, RtpHeaderBytes, read);
+ var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
+ if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
+ {
+ resTask.Dispose();
+ break;
+ }
+
+ // We don't want all these delay tasks to keep running
+ timeOutSource.Cancel();
+ var res = await resTask.ConfigureAwait(false);
+ var buffer = res.Buffer;
+
+ var read = buffer.Length - RtpHeaderBytes;
+
+ if (read > 0)
+ {
+ fileStream.Write(buffer, RtpHeaderBytes, read);
+ }
+
+ if (!resolved)
+ {
+ resolved = true;
+ DateOpened = DateTime.UtcNow;
+ openTaskCompletionSource.TrySetResult(true);
+ }
}
-
- if (!resolved)
- {
- resolved = true;
- DateOpened = DateTime.UtcNow;
- Resolve(openTaskCompletionSource);
- }
- }
- }
- }
-
- public class UdpClientStream : Stream
- {
- private static int RtpHeaderBytes = 12;
- private static int PacketSize = 1316;
- private readonly MediaBrowser.Model.Net.ISocket _udpClient;
- bool disposed;
-
- public UdpClientStream(MediaBrowser.Model.Net.ISocket udpClient) : base()
- {
- _udpClient = udpClient;
- }
-
- public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- {
- if (buffer == null)
- throw new ArgumentNullException(nameof(buffer));
-
- if (offset + count < 0)
- throw new ArgumentOutOfRangeException(nameof(offset), "offset + count must not be negative");
-
- if (offset + count > buffer.Length)
- throw new ArgumentException("offset + count must not be greater than the length of buffer");
-
- if (disposed)
- throw new ObjectDisposedException(nameof(UdpClientStream));
-
- // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize)
- // The RTP header will be stripped so see how many reads we need to make to fill the buffer.
- int numReads = count / PacketSize;
- int totalBytesRead = 0;
- byte[] receiveBuffer = new byte[81920];
-
- for (int i = 0; i < numReads; ++i)
- {
- var data = await _udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
-
- var bytesRead = data.ReceivedBytes - RtpHeaderBytes;
-
- // remove rtp header
- Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, buffer, offset, bytesRead);
- offset += bytesRead;
- totalBytesRead += bytesRead;
}
- return totalBytesRead;
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- if (buffer == null)
- throw new ArgumentNullException(nameof(buffer));
-
- if (offset + count < 0)
- throw new ArgumentOutOfRangeException("offset + count must not be negative", "offset+count");
-
- if (offset + count > buffer.Length)
- throw new ArgumentException("offset + count must not be greater than the length of buffer");
-
- if (disposed)
- throw new ObjectDisposedException(nameof(UdpClientStream));
-
- // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize)
- // The RTP header will be stripped so see how many reads we need to make to fill the buffer.
- int numReads = count / PacketSize;
- int totalBytesRead = 0;
- byte[] receiveBuffer = new byte[81920];
-
- for (int i = 0; i < numReads; ++i)
- {
- var receivedBytes = _udpClient.Receive(receiveBuffer, 0, receiveBuffer.Length);
-
- var bytesRead = receivedBytes - RtpHeaderBytes;
-
- // remove rtp header
- Buffer.BlockCopy(receiveBuffer, RtpHeaderBytes, buffer, offset, bytesRead);
- offset += bytesRead;
- totalBytesRead += bytesRead;
- }
- return totalBytesRead;
- }
-
- protected override void Dispose(bool disposing)
- {
- disposed = true;
- }
-
- public override bool CanRead => throw new NotImplementedException();
-
- public override bool CanSeek => throw new NotImplementedException();
-
- public override bool CanWrite => throw new NotImplementedException();
-
- public override long Length => throw new NotImplementedException();
-
- public override long Position
- {
- get => throw new NotImplementedException();
-
- set => throw new NotImplementedException();
- }
-
- public override void Flush()
- {
- throw new NotImplementedException();
- }
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- throw new NotImplementedException();
- }
-
- public override void SetLength(long value)
- {
- throw new NotImplementedException();
- }
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- throw new NotImplementedException();
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
new file mode 100644
index 000000000..153354932
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
@@ -0,0 +1,11 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public interface IHdHomerunChannelCommands
+ {
+ IEnumerable<(string, string)> GetCommands();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
new file mode 100644
index 000000000..26627b8aa
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
+ {
+ private string? _channel;
+ private string? _program;
+
+ public LegacyHdHomerunChannelCommands(string url)
+ {
+ // parse url for channel and program
+ var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
+ var match = regExp.Match(url);
+ if (match.Success)
+ {
+ _channel = match.Groups[1].Value;
+ _program = match.Groups[2].Value;
+ }
+ }
+
+ public IEnumerable<(string, string)> GetCommands()
+ {
+ if (!string.IsNullOrEmpty(_channel))
+ {
+ yield return ("channel", _channel);
+ }
+
+ if (!string.IsNullOrEmpty(_program))
+ {
+ yield return ("program", _program);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 1f8ca276e..5581ba87c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -1,10 +1,13 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
-using System.Collections.Generic;
+using System.Globalization;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
@@ -15,51 +18,64 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class LiveStream : ILiveStream
{
- public MediaSourceInfo OriginalMediaSource { get; set; }
- public MediaSourceInfo MediaSource { get; set; }
-
- public int ConsumerCount { get; set; }
-
- public string OriginalStreamId { get; set; }
- public bool EnableStreamSharing { get; set; }
- public string UniqueId { get; private set; }
-
- protected readonly IFileSystem FileSystem;
- protected readonly IServerApplicationPaths AppPaths;
-
- protected string TempFilePath;
- protected readonly ILogger Logger;
- protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
-
- public string TunerHostId { get; private set; }
-
- public DateTime DateOpened { get; protected set; }
-
- public Func<Task> OnClose { get; set; }
-
- public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths)
+ private readonly IConfigurationManager _configurationManager;
+
+ public LiveStream(
+ MediaSourceInfo mediaSource,
+ TunerHostInfo tuner,
+ IFileSystem fileSystem,
+ ILogger logger,
+ IConfigurationManager configurationManager,
+ IStreamHelper streamHelper)
{
OriginalMediaSource = mediaSource;
FileSystem = fileSystem;
MediaSource = mediaSource;
Logger = logger;
EnableStreamSharing = true;
- UniqueId = Guid.NewGuid().ToString("N");
+ UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
if (tuner != null)
{
TunerHostId = tuner.Id;
}
- AppPaths = appPaths;
+ _configurationManager = configurationManager;
+ StreamHelper = streamHelper;
ConsumerCount = 1;
SetTempFilePath("ts");
}
+ protected IFileSystem FileSystem { get; }
+
+ protected IStreamHelper StreamHelper { get; }
+
+ protected ILogger Logger { get; }
+
+ protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource();
+
+ protected string TempFilePath { get; set; }
+
+ public MediaSourceInfo OriginalMediaSource { get; set; }
+
+ public MediaSourceInfo MediaSource { get; set; }
+
+ public int ConsumerCount { get; set; }
+
+ public string OriginalStreamId { get; set; }
+
+ public bool EnableStreamSharing { get; set; }
+
+ public string UniqueId { get; }
+
+ public string TunerHostId { get; }
+
+ public DateTime DateOpened { get; protected set; }
+
protected void SetTempFilePath(string extension)
{
- TempFilePath = Path.Combine(AppPaths.GetTranscodingTempPath(), UniqueId + "." + extension);
+ TempFilePath = Path.Combine(_configurationManager.GetTranscodePath(), UniqueId + "." + extension);
}
public virtual Task Open(CancellationToken openCancellationToken)
@@ -72,164 +88,72 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
EnableStreamSharing = false;
- Logger.LogInformation("Closing " + GetType().Name);
+ Logger.LogInformation("Closing {Type}", GetType().Name);
LiveStreamCancellationTokenSource.Cancel();
- if (OnClose != null)
- {
- return CloseWithExternalFn();
- }
-
return Task.CompletedTask;
}
- private async Task CloseWithExternalFn()
+ public Stream GetStream()
{
- try
- {
- await OnClose().ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error closing live stream");
- }
- }
-
- protected Stream GetInputStream(string path, bool allowAsyncFileRead)
- {
- var fileOpenOptions = FileOpenOptions.SequentialScan;
-
- if (allowAsyncFileRead)
+ var stream = GetInputStream(TempFilePath);
+ bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
+ if (seekFile)
{
- fileOpenOptions |= FileOpenOptions.Asynchronous;
+ TrySeek(stream, -20000);
}
- return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions);
+ return stream;
}
- public Task DeleteTempFiles()
- {
- return DeleteTempFiles(GetStreamFilePaths());
- }
+ protected FileStream GetInputStream(string path)
+ => new FileStream(
+ path,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ IODefaults.FileStreamBufferSize,
+ FileOptions.SequentialScan | FileOptions.Asynchronous);
- protected async Task DeleteTempFiles(List<string> paths, int retryCount = 0)
+ protected async Task DeleteTempFiles(string path, int retryCount = 0)
{
if (retryCount == 0)
{
- Logger.LogInformation("Deleting temp files {0}", string.Join(", ", paths.ToArray()));
- }
-
- var failedFiles = new List<string>();
-
- foreach (var path in paths)
- {
- try
- {
- FileSystem.DeleteFile(path);
- }
- catch (DirectoryNotFoundException)
- {
- }
- catch (FileNotFoundException)
- {
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error deleting file {path}", path);
- failedFiles.Add(path);
- }
+ Logger.LogInformation("Deleting temp file {FilePath}", path);
}
- if (failedFiles.Count > 0 && retryCount <= 40)
- {
- await Task.Delay(500).ConfigureAwait(false);
- await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false);
- }
- }
-
- protected virtual List<string> GetStreamFilePaths()
- {
- return new List<string> { TempFilePath };
- }
-
- public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
- {
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token;
-
- var allowAsync = false;
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-
- bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
-
- var nextFileInfo = GetNextFile(null);
- var nextFile = nextFileInfo.Item1;
- var isLastFile = nextFileInfo.Item2;
-
- while (!string.IsNullOrEmpty(nextFile))
+ try
{
- var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
-
- await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false);
-
- seekFile = false;
- nextFileInfo = GetNextFile(nextFile);
- nextFile = nextFileInfo.Item1;
- isLastFile = nextFileInfo.Item2;
+ FileSystem.DeleteFile(path);
}
-
- Logger.LogInformation("Live Stream ended.");
- }
-
- private Tuple<string, bool> GetNextFile(string currentFile)
- {
- var files = GetStreamFilePaths();
-
- //logger.LogInformation("Live stream files: {0}", string.Join(", ", files.ToArray()));
-
- if (string.IsNullOrEmpty(currentFile))
+ catch (Exception ex)
{
- return new Tuple<string, bool>(files.Last(), true);
+ Logger.LogError(ex, "Error deleting file {FilePath}", path);
+ if (retryCount <= 40)
+ {
+ await Task.Delay(500).ConfigureAwait(false);
+ await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
+ }
}
-
- var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
-
- var isLastFile = nextIndex == files.Count - 1;
-
- return new Tuple<string, bool>(files.ElementAtOrDefault(nextIndex), isLastFile);
}
- private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
+ private void TrySeek(Stream stream, long offset)
{
- //logger.LogInformation("Opening live stream file {0}. Empty read limit: {1}", path, emptyReadLimit);
-
- using (var inputStream = (FileStream)GetInputStream(path, allowAsync))
+ if (!stream.CanSeek)
{
- if (seekFile)
- {
- TrySeek(inputStream, -20000);
- }
-
- await ApplicationHost.StreamHelper.CopyToAsync(inputStream, stream, 81920, emptyReadLimit, cancellationToken).ConfigureAwait(false);
+ return;
}
- }
-
- protected virtual int EmptyReadLimit => 1000;
- private void TrySeek(FileStream stream, long offset)
- {
- //logger.LogInformation("TrySeek live stream");
try
{
stream.Seek(offset, SeekOrigin.End);
}
catch (IOException)
{
-
}
catch (ArgumentException)
{
-
}
catch (Exception ex)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index fdaaf0bae..08b9260b9 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -1,7 +1,13 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
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;
@@ -10,32 +16,50 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.System;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
- private readonly IHttpClient _httpClient;
+ private static readonly string[] _disallowedSharedStreamExtensions =
+ {
+ ".mkv",
+ ".mp4",
+ ".m3u8",
+ ".mpd"
+ };
+
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
private readonly IMediaSourceManager _mediaSourceManager;
-
- public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, INetworkManager networkManager)
- : base(config, logger, jsonSerializer, fileSystem)
+ private readonly IStreamHelper _streamHelper;
+
+ public M3UTunerHost(
+ IServerConfigurationManager config,
+ IMediaSourceManager mediaSourceManager,
+ ILogger<M3UTunerHost> logger,
+ IFileSystem fileSystem,
+ IHttpClientFactory httpClientFactory,
+ IServerApplicationHost appHost,
+ INetworkManager networkManager,
+ IStreamHelper streamHelper,
+ IMemoryCache memoryCache)
+ : base(config, logger, fileSystem, memoryCache)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
_networkManager = networkManager;
_mediaSourceManager = mediaSourceManager;
+ _streamHelper = streamHelper;
}
public override string Type => "m3u";
@@ -44,16 +68,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetFullChannelIdPrefix(TunerHostInfo info)
{
- return ChannelIdPrefix + info.Url.GetMD5().ToString("N");
+ return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
- var channelIdPrefix = GetFullChannelIdPrefix(info);
+ var channelIdPrefix = GetFullChannelIdPrefix(tuner);
- var result = await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
-
- return result.Cast<ChannelInfo>().ToList();
+ return await new M3uParser(Logger, _httpClientFactory)
+ .Parse(tuner, channelIdPrefix, cancellationToken)
+ .ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -64,7 +88,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Name = Name,
SourceType = Type,
Status = LiveTvTunerStatus.Available,
- Id = i.Url.GetMD5().ToString("N"),
+ Id = i.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture),
Url = i.Url
})
.ToList();
@@ -72,32 +96,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private string[] _disallowedSharedStreamExtensions = new string[]
+ protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
- };
-
- protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
- {
- var tunerCount = info.TunerCount;
+ var tunerCount = tunerHost.TunerCount;
if (tunerCount > 0)
{
- var tunerHostId = info.Id;
- var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)).ToList();
+ var tunerHostId = tunerHost.Id;
+ var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
- if (liveStreams.Count >= tunerCount)
+ if (liveStreams.Count() >= tunerCount)
{
throw new LiveTvConflictException("M3U simultaneous stream limit has been reached.");
}
}
- var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
+ var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false);
- var mediaSource = sources.First();
+ var mediaSource = sources[0];
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
{
@@ -105,24 +121,23 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
- return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
+ return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}
- return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths);
+ return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper);
}
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).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
-
}
}
- protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
+ protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{
- return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channelInfo) });
+ return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) });
}
protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel)
@@ -145,7 +160,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (protocol == MediaProtocol.Http)
{
// Use user-defined user-agent. If there isn't one, make it look like a browser.
- httpHeaders["User-Agent"] = string.IsNullOrWhiteSpace(info.UserAgent) ?
+ httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ?
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" :
info.UserAgent;
}
@@ -176,7 +191,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
ReadAtNativeFramerate = false,
- Id = channel.Path.GetMD5().ToString("N"),
+ Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture),
IsInfiniteStream = true,
IsRemote = isRemote,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index ad124bb0f..506ef5548 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -1,107 +1,112 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
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;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3uParser
{
+ private const string ExtInfPrefix = "#EXTINF:";
+
private readonly ILogger _logger;
- private readonly IHttpClient _httpClient;
- private readonly IServerApplicationHost _appHost;
+ private readonly IHttpClientFactory _httpClientFactory;
- public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
+ public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
- _httpClient = httpClient;
- _appHost = appHost;
+ _httpClientFactory = httpClientFactory;
}
- public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
+ public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{
// Read the file and display it line by line.
- using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
+ using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
- return GetChannels(reader, channelIdPrefix, tunerHostId);
+ return await GetChannelsAsync(reader, channelIdPrefix, info.Id).ConfigureAwait(false);
}
}
- public List<ChannelInfo> ParseString(string text, string channelIdPrefix, string tunerHostId)
+ public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{
- // Read the file and display it line by line.
- using (var reader = new StringReader(text))
+ if (info == null)
{
- return GetChannels(reader, channelIdPrefix, tunerHostId);
+ throw new ArgumentNullException(nameof(info));
}
- }
- public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
- {
- if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ if (!info.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 AsyncFile.OpenRead(info.Url);
+ }
+
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+ if (!string.IsNullOrEmpty(info.UserAgent))
+ {
+ requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
}
- return Task.FromResult((Stream)File.OpenRead(url));
+
+ // Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadAsStreamAsync(cancellationToken);
}
- const string ExtInfPrefix = "#EXTINF:";
- private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
+ private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
{
var channels = new List<ChannelInfo>();
- string line;
- string extInf = "";
+ string extInf = string.Empty;
- while ((line = reader.ReadLine()) != null)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- line = line.Trim();
- if (string.IsNullOrWhiteSpace(line))
+ var trimmedLine = line.Trim();
+ if (string.IsNullOrWhiteSpace(trimmedLine))
{
continue;
}
- if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
+ if (trimmedLine.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
{
continue;
}
- if (line.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
+ if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
{
- extInf = line.Substring(ExtInfPrefix.Length).Trim();
- _logger.LogInformation("Found m3u channel: {0}", extInf);
+ extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
}
- else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith("#", StringComparison.OrdinalIgnoreCase))
+ else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
- var channel = GetChannelnfo(extInf, tunerHostId, line);
+ var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id))
{
- channel.Id = channelIdPrefix + line.GetMD5().ToString("N");
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
- channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N");
+ channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- channel.Path = line;
+ channel.Path = trimmedLine;
channels.Add(channel);
- extInf = "";
+ _logger.LogInformation("Parsed channel: {ChannelName}", channel.Name);
+ extInf = string.Empty;
}
}
@@ -110,8 +115,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
{
- var channel = new ChannelInfo();
- channel.TunerHostId = tunerHostId;
+ var channel = new ChannelInfo()
+ {
+ TunerHostId = tunerHostId
+ };
extInf = extInf.Trim();
@@ -123,6 +130,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.ImageUrl = value;
}
+ if (attributes.TryGetValue("group-title", out string groupTitle))
+ {
+ channel.ChannelGroup = groupTitle;
+ }
+
channel.Name = GetChannelName(extInf, attributes);
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
@@ -137,13 +149,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
channelIdValues.Add(channelId);
}
+
if (!string.IsNullOrWhiteSpace(tvgId))
{
channelIdValues.Add(tvgId);
}
+
if (channelIdValues.Count > 0)
{
- channel.Id = string.Join("_", channelIdValues.ToArray());
+ channel.Id = string.Join('_', channelIdValues);
}
return channel;
@@ -151,16 +165,15 @@ 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.Last().Trim() : null;
+ var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ 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;
}
@@ -170,41 +183,40 @@ 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
+ // where 5 isn't 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();
}
}
}
}
-
}
if (!IsValidChannelNumber(numberString))
@@ -226,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
if (!IsValidChannelNumber(numberString))
{
@@ -253,7 +265,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;
}
@@ -263,8 +275,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
{
- var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null;
+ var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null;
// Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz
@@ -276,19 +288,19 @@ 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();
+ // channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
}
}
}
- attributes.TryGetValue("tvg-name", out string name);
+ string name = nameInExtInf;
if (string.IsNullOrWhiteSpace(name))
{
- name = nameInExtInf;
+ attributes.TryGetValue("tvg-name", out name);
}
if (string.IsNullOrWhiteSpace(name))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index d74cf3be2..3b69e55b0 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -1,8 +1,14 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
-using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
@@ -16,13 +22,22 @@ 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(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
- : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths)
+ public SharedHttpStream(
+ MediaSourceInfo mediaSource,
+ TunerHostInfo tunerHostInfo,
+ string originalStreamId,
+ IFileSystem fileSystem,
+ 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;
@@ -39,105 +54,95 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
var typeName = GetType().Name;
- Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
-
- var httpRequestOptions = new HttpRequestOptions
- {
- Url = url,
- CancellationToken = CancellationToken.None,
- BufferContent = false,
-
- // Increase a little bit
- TimeoutMs = 30000,
-
- EnableHttpCompression = false,
-
- LogResponse = true,
- LogResponseHeaders = true
- };
-
- foreach (var header in mediaSource.RequiredHttpHeaders)
- {
- httpRequestOptions.RequestHeaders[header.Key] = header.Value;
- }
-
- var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false);
-
- var extension = "ts";
- var requiresRemux = false;
-
- var contentType = response.ContentType ?? string.Empty;
- if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
- else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("dash", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("mpegURL", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("text/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
-
- // Close the stream without any sharing features
- if (requiresRemux)
+ Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
+
+ // Response stream is disposed manually.
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
+ if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
{
- using (response)
- {
- return;
- }
+ // Close the stream without any sharing features
+ response.Dispose();
+ return;
}
- SetTempFilePath(extension);
+ SetTempFilePath("ts");
var taskCompletionSource = new TaskCompletionSource<bool>();
- var now = DateTime.UtcNow;
-
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
- //OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.Path = tempFile;
- //OpenedMediaSource.ReadAtNativeFramerate = true;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.Path = tempFile;
+ // OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
- //OpenedMediaSource.Path = TempFilePath;
- //OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.Path = TempFilePath;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.Path = _tempFilePath;
- //OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.SupportsDirectPlay = false;
- //OpenedMediaSource.SupportsDirectStream = true;
- //OpenedMediaSource.SupportsTranscoding = true;
+ // OpenedMediaSource.Path = _tempFilePath;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.SupportsDirectPlay = false;
+ // OpenedMediaSource.SupportsDirectStream = true;
+ // OpenedMediaSource.SupportsTranscoding = true;
await taskCompletionSource.Task.ConfigureAwait(false);
+ if (taskCompletionSource.Task.Exception != null)
+ {
+ // Error happened while opening the stream so raise the exception again to inform the caller
+ throw taskCompletionSource.Task.Exception;
+ }
+
+ if (!taskCompletionSource.Task.Result)
+ {
+ Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
+ throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
+ }
}
- 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
+ return Task.Run(
+ async () =>
{
- Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
- using (response)
- using (var stream = response.Content)
- using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None))
+ try
{
- await ApplicationHost.StreamHelper.CopyToAsync(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false);
+ Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
+ using var message = response;
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ IODefaults.CopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
}
- }
- catch (OperationCanceledException)
- {
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error copying live stream.");
- }
- EnableStreamSharing = false;
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
- });
+ catch (OperationCanceledException ex)
+ {
+ Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+
+ openTaskCompletionSource.TrySetResult(false);
+
+ EnableStreamSharing = false;
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
+ },
+ CancellationToken.None);
}
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)