Explorar o código

Add IListingsManager service

Patrick Barron hai 1 ano
pai
achega
42b052a5a6

+ 12 - 38
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -45,6 +45,7 @@ public class LiveTvController : BaseJellyfinApiController
     private readonly ILiveTvManager _liveTvManager;
     private readonly IGuideManager _guideManager;
     private readonly ITunerHostManager _tunerHostManager;
+    private readonly IListingsManager _listingsManager;
     private readonly IUserManager _userManager;
     private readonly IHttpClientFactory _httpClientFactory;
     private readonly ILibraryManager _libraryManager;
@@ -59,6 +60,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
     /// <param name="guideManager">Instance of the <see cref="IGuideManager"/> interface.</param>
     /// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param>
+    /// <param name="listingsManager">Instance of the <see cref="IListingsManager"/> interface.</param>
     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
     /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
@@ -70,6 +72,7 @@ public class LiveTvController : BaseJellyfinApiController
         ILiveTvManager liveTvManager,
         IGuideManager guideManager,
         ITunerHostManager tunerHostManager,
+        IListingsManager listingsManager,
         IUserManager userManager,
         IHttpClientFactory httpClientFactory,
         ILibraryManager libraryManager,
@@ -81,6 +84,7 @@ public class LiveTvController : BaseJellyfinApiController
         _liveTvManager = liveTvManager;
         _guideManager = guideManager;
         _tunerHostManager = tunerHostManager;
+        _listingsManager = listingsManager;
         _userManager = userManager;
         _httpClientFactory = httpClientFactory;
         _libraryManager = libraryManager;
@@ -1015,7 +1019,7 @@ public class LiveTvController : BaseJellyfinApiController
             listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
         }
 
-        return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
+        return await _listingsManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
     }
 
     /// <summary>
@@ -1029,7 +1033,7 @@ public class LiveTvController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public ActionResult DeleteListingProvider([FromQuery] string? id)
     {
-        _liveTvManager.DeleteListingsProvider(id);
+        _listingsManager.DeleteListingsProvider(id);
         return NoContent();
     }
 
@@ -1050,9 +1054,7 @@ public class LiveTvController : BaseJellyfinApiController
         [FromQuery] string? type,
         [FromQuery] string? location,
         [FromQuery] string? country)
-    {
-        return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
-    }
+        => await _listingsManager.GetLineups(type, id, country, location).ConfigureAwait(false);
 
     /// <summary>
     /// Gets available countries.
@@ -1083,48 +1085,20 @@ public class LiveTvController : BaseJellyfinApiController
     [HttpGet("ChannelMappingOptions")]
     [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
-    {
-        var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
-
-        var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
-
-        var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
-
-        var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
-            .ConfigureAwait(false);
-
-        var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
-            .ConfigureAwait(false);
-
-        var mappings = listingsProviderInfo.ChannelMappings;
-
-        return new ChannelMappingOptionsDto
-        {
-            TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
-            ProviderChannels = providerChannels.Select(i => new NameIdPair
-            {
-                Name = i.Name,
-                Id = i.Id
-            }).ToList(),
-            Mappings = mappings,
-            ProviderName = listingsProviderName
-        };
-    }
+    public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
+        => _listingsManager.GetChannelMappingOptions(providerId);
 
     /// <summary>
     /// Set channel mappings.
     /// </summary>
-    /// <param name="setChannelMappingDto">The set channel mapping dto.</param>
+    /// <param name="dto">The set channel mapping dto.</param>
     /// <response code="200">Created channel mapping returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
     [HttpPost("ChannelMappings")]
     [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
-    {
-        return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
-    }
+    public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
+        => _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
 
     /// <summary>
     /// Get tuner host types.

+ 79 - 0
MediaBrowser.Controller/LiveTv/IListingsManager.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv;
+
+/// <summary>
+/// Service responsible for managing <see cref="IListingsProvider"/>s and mapping
+/// their channels to channels provided by <see cref="ITunerHost"/>s.
+/// </summary>
+public interface IListingsManager
+{
+    /// <summary>
+    /// Saves the listing provider.
+    /// </summary>
+    /// <param name="info">The listing provider information.</param>
+    /// <param name="validateLogin">A value indicating whether to validate login.</param>
+    /// <param name="validateListings">A value indicating whether to validate listings..</param>
+    /// <returns>Task.</returns>
+    Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
+
+    /// <summary>
+    /// Deletes the listing provider.
+    /// </summary>
+    /// <param name="id">The listing provider's id.</param>
+    void DeleteListingsProvider(string? id);
+
+    /// <summary>
+    /// Gets the lineups.
+    /// </summary>
+    /// <param name="providerType">Type of the provider.</param>
+    /// <param name="providerId">The provider identifier.</param>
+    /// <param name="country">The country.</param>
+    /// <param name="location">The location.</param>
+    /// <returns>The available lineups.</returns>
+    Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location);
+
+    /// <summary>
+    /// Gets the programs for a provided channel.
+    /// </summary>
+    /// <param name="channel">The channel to retrieve programs for.</param>
+    /// <param name="startDateUtc">The earliest date to retrieve programs for.</param>
+    /// <param name="endDateUtc">The latest date to retrieve programs for.</param>
+    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+    /// <returns>The available programs.</returns>
+    Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
+        ChannelInfo channel,
+        DateTime startDateUtc,
+        DateTime endDateUtc,
+        CancellationToken cancellationToken);
+
+    /// <summary>
+    /// Adds metadata from the <see cref="IListingsProvider"/>s to the provided channels.
+    /// </summary>
+    /// <param name="channels">The channels.</param>
+    /// <param name="enableCache">A value indicating whether to use the EPG channel cache.</param>
+    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+    /// <returns>A task representing the metadata population.</returns>
+    Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationToken);
+
+    /// <summary>
+    /// Gets the channel mapping options for a provider.
+    /// </summary>
+    /// <param name="providerId">The id of the provider to use.</param>
+    /// <returns>The channel mapping options.</returns>
+    Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId);
+
+    /// <summary>
+    /// Sets the channel mapping.
+    /// </summary>
+    /// <param name="providerId">The id of the provider for the mapping.</param>
+    /// <param name="tunerChannelNumber">The tuner channel number.</param>
+    /// <param name="providerChannelNumber">The provider channel number.</param>
+    /// <returns>The updated channel mapping.</returns>
+    Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
+}

+ 0 - 31
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -36,8 +36,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The services.</value>
         IReadOnlyList<ILiveTvService> Services { get; }
 
-        IReadOnlyList<IListingsProvider> ListingProviders { get; }
-
         /// <summary>
         /// Gets the new timer defaults asynchronous.
         /// </summary>
@@ -239,31 +237,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task.</returns>
         Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
 
-        /// <summary>
-        /// Saves the listing provider.
-        /// </summary>
-        /// <param name="info">The information.</param>
-        /// <param name="validateLogin">if set to <c>true</c> [validate login].</param>
-        /// <param name="validateListings">if set to <c>true</c> [validate listings].</param>
-        /// <returns>Task.</returns>
-        Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
-
-        void DeleteListingsProvider(string id);
-
-        Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
-
-        TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels);
-
-        /// <summary>
-        /// Gets the lineups.
-        /// </summary>
-        /// <param name="providerType">Type of the provider.</param>
-        /// <param name="providerId">The provider identifier.</param>
-        /// <param name="country">The country.</param>
-        /// <param name="location">The location.</param>
-        /// <returns>Task&lt;List&lt;NameIdPair&gt;&gt;.</returns>
-        Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location);
-
         /// <summary>
         /// Adds the channel information.
         /// </summary>
@@ -272,10 +245,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="user">The user.</param>
         void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user);
 
-        Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
-
-        Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
-
         string GetEmbyTvActiveRecordingPath(string id);
 
         ActiveRecordingInfo GetActiveRecordingInfo(string path);

+ 0 - 17
MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs

@@ -1,17 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.LiveTv
-{
-    public class TunerChannelMapping
-    {
-        public string Name { get; set; }
-
-        public string ProviderChannelName { get; set; }
-
-        public string ProviderChannelId { get; set; }
-
-        public string Id { get; set; }
-    }
-}

+ 1 - 2
Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs → MediaBrowser.Model/LiveTv/ChannelMappingOptionsDto.cs

@@ -1,9 +1,8 @@
 using System;
 using System.Collections.Generic;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 
-namespace Jellyfin.Api.Models.LiveTvDtos;
+namespace MediaBrowser.Model.LiveTv;
 
 /// <summary>
 /// Channel mapping options dto.

+ 16 - 0
MediaBrowser.Model/LiveTv/TunerChannelMapping.cs

@@ -0,0 +1,16 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.LiveTv;
+
+public class TunerChannelMapping
+{
+    public string Name { get; set; }
+
+    public string ProviderChannelName { get; set; }
+
+    public string ProviderChannelId { get; set; }
+
+    public string Id { get; set; }
+}

+ 10 - 275
src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs

@@ -61,14 +61,11 @@ namespace Jellyfin.LiveTv.EmbyTV
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IStreamHelper _streamHelper;
         private readonly LiveTvDtoService _tvDtoService;
-        private readonly IListingsProvider[] _listingsProviders;
+        private readonly IListingsManager _listingsManager;
 
         private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
             new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
 
-        private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
-            new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
-
         private readonly AsyncNonKeyedLocker _recordingDeleteSemaphore = new(1);
 
         private bool _disposed;
@@ -86,7 +83,7 @@ namespace Jellyfin.LiveTv.EmbyTV
             IProviderManager providerManager,
             IMediaEncoder mediaEncoder,
             LiveTvDtoService tvDtoService,
-            IEnumerable<IListingsProvider> listingsProviders)
+            IListingsManager listingsManager)
         {
             Current = this;
 
@@ -102,7 +99,7 @@ namespace Jellyfin.LiveTv.EmbyTV
             _tunerHostManager = tunerHostManager;
             _mediaSourceManager = mediaSourceManager;
             _streamHelper = streamHelper;
-            _listingsProviders = listingsProviders.ToArray();
+            _listingsManager = listingsManager;
 
             _seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
             _timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
@@ -312,15 +309,15 @@ namespace Jellyfin.LiveTv.EmbyTV
 
         private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
         {
-            var list = new List<ChannelInfo>();
+            var channels = new List<ChannelInfo>();
 
             foreach (var hostInstance in _tunerHostManager.TunerHosts)
             {
                 try
                 {
-                    var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
+                    var tunerChannels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
 
-                    list.AddRange(channels);
+                    channels.AddRange(tunerChannels);
                 }
                 catch (Exception ex)
                 {
@@ -328,209 +325,9 @@ namespace Jellyfin.LiveTv.EmbyTV
                 }
             }
 
-            foreach (var provider in GetListingProviders())
-            {
-                var enabledChannels = list
-                    .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
-                    .ToList();
-
-                if (enabledChannels.Count > 0)
-                {
-                    try
-                    {
-                        await AddMetadata(provider.Item1, provider.Item2, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false);
-                    }
-                    catch (NotSupportedException)
-                    {
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error adding metadata");
-                    }
-                }
-            }
-
-            return list;
-        }
-
-        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);
-
-            foreach (var tunerChannel in tunerChannels)
-            {
-                var epgChannel = GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
-
-                if (epgChannel is not null)
-                {
-                    if (!string.IsNullOrWhiteSpace(epgChannel.Name))
-                    {
-                        // tunerChannel.Name = epgChannel.Name;
-                    }
-
-                    if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
-                    {
-                        tunerChannel.ImageUrl = epgChannel.ImageUrl;
-                    }
-                }
-            }
-        }
-
-        private async Task<EpgChannelData> GetEpgChannels(
-            IListingsProvider provider,
-            ListingsProviderInfo info,
-            bool enableCache,
-            CancellationToken cancellationToken)
-        {
-            if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result))
-            {
-                var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
-
-                foreach (var channel in channels)
-                {
-                    _logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
-                }
-
-                result = new EpgChannelData(channels);
-                _epgChannels.AddOrUpdate(info.Id, result, (_, _) => 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);
-
-            return GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels);
-        }
-
-        private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
-        {
-            foreach (NameValuePair mapping in mappings)
-            {
-                if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
-                {
-                    return mapping.Value;
-                }
-            }
-
-            return channelId;
-        }
-
-        internal ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
-        {
-            return GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(epgChannels));
-        }
+            await _listingsManager.AddProviderMetadata(channels, enableCache, cancellationToken).ConfigureAwait(false);
 
-        private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, EpgChannelData epgChannels)
-        {
-            return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
-        }
-
-        private ChannelInfo GetEpgChannelFromTunerChannel(
-            NameValuePair[] mappings,
-            ChannelInfo tunerChannel,
-            EpgChannelData epgChannelData)
-        {
-            if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
-            {
-                var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
-
-                if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
-                {
-                    mappedTunerChannelId = tunerChannel.Id;
-                }
-
-                var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
-
-                if (channel is not null)
-                {
-                    return channel;
-                }
-            }
-
-            if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
-            {
-                var tunerChannelId = tunerChannel.TunerChannelId;
-                if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
-                {
-                    tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
-                }
-
-                var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
-
-                if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
-                {
-                    mappedTunerChannelId = tunerChannelId;
-                }
-
-                var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
-
-                if (channel is not null)
-                {
-                    return channel;
-                }
-            }
-
-            if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
-            {
-                var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
-
-                if (string.IsNullOrWhiteSpace(tunerChannelNumber))
-                {
-                    tunerChannelNumber = tunerChannel.Number;
-                }
-
-                var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
-
-                if (channel is not null)
-                {
-                    return channel;
-                }
-            }
-
-            if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
-            {
-                var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
-
-                var channel = epgChannelData.GetChannelByName(normalizedName);
-
-                if (channel is not null)
-                {
-                    return channel;
-                }
-            }
-
-            return null;
-        }
-
-        public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
-        {
-            var list = new List<ChannelInfo>();
-
-            foreach (var hostInstance in _tunerHostManager.TunerHosts)
-            {
-                try
-                {
-                    var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
-
-                    list.AddRange(channels);
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error getting channels");
-                }
-            }
-
-            return list
-                .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId))
-                .ToList();
+            return channels;
         }
 
         public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
@@ -877,75 +674,13 @@ namespace Jellyfin.LiveTv.EmbyTV
             return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
         }
 
-        private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
-        {
-            if (info.EnableAllTuners)
-            {
-                return true;
-            }
-
-            if (string.IsNullOrWhiteSpace(tunerHostId))
-            {
-                throw new ArgumentNullException(nameof(tunerHostId));
-            }
-
-            return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase);
-        }
-
         public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
         {
             var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
             var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
 
-            foreach (var provider in GetListingProviders())
-            {
-                if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
-                {
-                    _logger.LogDebug("Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner.", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
-                    continue;
-                }
-
-                _logger.LogDebug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
-
-                var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
-
-                if (epgChannel is 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);
-                    continue;
-                }
-
-                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)
-                {
-                    program.ChannelId = channelId;
-
-                    program.Id += "_" + channelId;
-                }
-
-                if (programs.Count > 0)
-                {
-                    return programs;
-                }
-            }
-
-            return Enumerable.Empty<ProgramInfo>();
-        }
-
-        private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
-        {
-            return _config.GetLiveTvConfiguration().ListingProviders
-                .Select(i =>
-                {
-                    var provider = _listingsProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
-
-                    return provider is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
-                })
-                .Where(i => i is not null)
-                .ToList();
+            return await _listingsManager.GetProgramsAsync(channel, startDateUtc, endDateUtc, cancellationToken)
+                .ConfigureAwait(false);
         }
 
         public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)

+ 1 - 0
src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs

@@ -26,6 +26,7 @@ public static class LiveTvServiceCollectionExtensions
         services.AddSingleton<IChannelManager, ChannelManager>();
         services.AddSingleton<IStreamHelper, StreamHelper>();
         services.AddSingleton<ITunerHostManager, TunerHostManager>();
+        services.AddSingleton<IListingsManager, ListingsManager>();
         services.AddSingleton<IGuideManager, GuideManager>();
 
         services.AddSingleton<ILiveTvService, EmbyTV.EmbyTV>();

+ 470 - 0
src/Jellyfin.LiveTv/Listings/ListingsManager.cs

@@ -0,0 +1,470 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.EmbyTV;
+using Jellyfin.LiveTv.Guide;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.LiveTv.Listings;
+
+/// <inheritdoc />
+public class ListingsManager : IListingsManager
+{
+    private readonly ILogger<ListingsManager> _logger;
+    private readonly IConfigurationManager _config;
+    private readonly ITaskManager _taskManager;
+    private readonly ITunerHostManager _tunerHostManager;
+    private readonly IListingsProvider[] _listingsProviders;
+
+    private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = new(StringComparer.OrdinalIgnoreCase);
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ListingsManager"/> class.
+    /// </summary>
+    /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
+    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+    /// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
+    /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
+    /// <param name="listingsProviders">The <see cref="IListingsProvider"/>.</param>
+    public ListingsManager(
+        ILogger<ListingsManager> logger,
+        IConfigurationManager config,
+        ITaskManager taskManager,
+        ITunerHostManager tunerHostManager,
+        IEnumerable<IListingsProvider> listingsProviders)
+    {
+        _logger = logger;
+        _config = config;
+        _taskManager = taskManager;
+        _tunerHostManager = tunerHostManager;
+        _listingsProviders = listingsProviders.ToArray();
+    }
+
+    /// <inheritdoc />
+    public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
+    {
+        ArgumentNullException.ThrowIfNull(info);
+
+        // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
+        // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
+        info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info))!;
+
+        var provider = GetProvider(info.Type);
+        await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
+
+        var config = _config.GetLiveTvConfiguration();
+
+        var list = config.ListingProviders.ToList();
+        int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
+
+        if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
+        {
+            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            list.Add(info);
+            config.ListingProviders = list.ToArray();
+        }
+        else
+        {
+            config.ListingProviders[index] = info;
+        }
+
+        _config.SaveConfiguration("livetv", config);
+        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
+
+        return info;
+    }
+
+    /// <inheritdoc />
+    public void DeleteListingsProvider(string? id)
+    {
+        var config = _config.GetLiveTvConfiguration();
+
+        config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
+
+        _config.SaveConfiguration("livetv", config);
+        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
+    }
+
+    /// <inheritdoc />
+    public Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location)
+    {
+        if (string.IsNullOrWhiteSpace(providerId))
+        {
+            return GetProvider(providerType).GetLineups(null, country, location);
+        }
+
+        var info = _config.GetLiveTvConfiguration().ListingProviders
+            .FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase))
+            ?? throw new ResourceNotFoundException();
+
+        return GetProvider(info.Type).GetLineups(info, country, location);
+    }
+
+    /// <inheritdoc />
+    public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
+        ChannelInfo channel,
+        DateTime startDateUtc,
+        DateTime endDateUtc,
+        CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(channel);
+
+        foreach (var (provider, providerInfo) in GetListingProviders())
+        {
+            if (!IsListingProviderEnabledForTuner(providerInfo, channel.TunerHostId))
+            {
+                _logger.LogDebug(
+                    "Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner.",
+                    channel.Number,
+                    channel.Name,
+                    provider.Name,
+                    providerInfo.ListingsId ?? string.Empty);
+                continue;
+            }
+
+            _logger.LogDebug(
+                "Getting programs for channel {0}-{1} from {2}-{3}",
+                channel.Number,
+                channel.Name,
+                provider.Name,
+                providerInfo.ListingsId ?? string.Empty);
+
+            var epgChannels = await GetEpgChannels(provider, providerInfo, true, cancellationToken).ConfigureAwait(false);
+
+            var epgChannel = GetEpgChannelFromTunerChannel(providerInfo.ChannelMappings, channel, epgChannels);
+            if (epgChannel is null)
+            {
+                _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Name, providerInfo.ListingsId ?? string.Empty);
+                continue;
+            }
+
+            var programs = (await provider
+                .GetProgramsAsync(providerInfo, 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)
+            {
+                program.ChannelId = channel.Id;
+                program.Id += "_" + channel.Id;
+            }
+
+            if (programs.Count > 0)
+            {
+                return programs;
+            }
+        }
+
+        return Enumerable.Empty<ProgramInfo>();
+    }
+
+    /// <inheritdoc />
+    public async Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(channels);
+
+        foreach (var (provider, providerInfo) in GetListingProviders())
+        {
+            var enabledChannels = channels
+                .Where(i => IsListingProviderEnabledForTuner(providerInfo, i.TunerHostId))
+                .ToList();
+
+            if (enabledChannels.Count == 0)
+            {
+                continue;
+            }
+
+            try
+            {
+                await AddMetadata(provider, providerInfo, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false);
+            }
+            catch (NotSupportedException)
+            {
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error adding metadata");
+            }
+        }
+    }
+
+    /// <inheritdoc />
+    public async Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId)
+    {
+        var listingsProviderInfo = _config.GetLiveTvConfiguration().ListingProviders
+            .First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
+
+        var provider = GetProvider(listingsProviderInfo.Type);
+
+        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
+            .ConfigureAwait(false);
+
+        var providerChannels = await provider.GetChannels(listingsProviderInfo, default)
+            .ConfigureAwait(false);
+
+        var mappings = listingsProviderInfo.ChannelMappings;
+
+        return new ChannelMappingOptionsDto
+        {
+            TunerChannels = tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
+            ProviderChannels = providerChannels.Select(i => new NameIdPair
+            {
+                Name = i.Name,
+                Id = i.Id
+            }).ToList(),
+            Mappings = mappings,
+            ProviderName = provider.Name
+        };
+    }
+
+    /// <inheritdoc />
+    public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
+    {
+        var config = _config.GetLiveTvConfiguration();
+
+        var listingsProviderInfo = config.ListingProviders
+            .First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
+
+        listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
+            .Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
+
+        if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
+        {
+            var list = listingsProviderInfo.ChannelMappings.ToList();
+            list.Add(new NameValuePair
+            {
+                Name = tunerChannelNumber,
+                Value = providerChannelNumber
+            });
+            listingsProviderInfo.ChannelMappings = list.ToArray();
+        }
+
+        _config.SaveConfiguration("livetv", config);
+
+        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
+            .ConfigureAwait(false);
+
+        var providerChannels = await GetProvider(listingsProviderInfo.Type).GetChannels(listingsProviderInfo, default)
+            .ConfigureAwait(false);
+
+        var tunerChannelMappings = tunerChannels
+            .Select(i => GetTunerChannelMapping(i, listingsProviderInfo.ChannelMappings, providerChannels)).ToList();
+
+        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
+
+        return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
+    }
+
+    private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
+        => _config.GetLiveTvConfiguration().ListingProviders
+            .Select(i =>
+            {
+                var provider = _listingsProviders
+                    .FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+                return provider is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
+            })
+            .Where(i => i is not null)
+            .ToList()!; // Already filtered out null
+
+    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);
+
+        foreach (var tunerChannel in tunerChannels)
+        {
+            var epgChannel = GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
+            if (epgChannel is null)
+            {
+                continue;
+            }
+
+            if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
+            {
+                tunerChannel.ImageUrl = epgChannel.ImageUrl;
+            }
+        }
+    }
+
+    private static bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
+    {
+        if (info.EnableAllTuners)
+        {
+            return true;
+        }
+
+        ArgumentException.ThrowIfNullOrWhiteSpace(tunerHostId);
+
+        return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
+    }
+
+    private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
+    {
+        foreach (NameValuePair mapping in mappings)
+        {
+            if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
+            {
+                return mapping.Value;
+            }
+        }
+
+        return channelId;
+    }
+
+    private async Task<EpgChannelData> GetEpgChannels(
+        IListingsProvider provider,
+        ListingsProviderInfo info,
+        bool enableCache,
+        CancellationToken cancellationToken)
+    {
+        if (enableCache && _epgChannels.TryGetValue(info.Id, out var result))
+        {
+            return result;
+        }
+
+        var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
+        foreach (var channel in channels)
+        {
+            _logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
+        }
+
+        result = new EpgChannelData(channels);
+        _epgChannels.AddOrUpdate(info.Id, result, (_, _) => result);
+
+        return result;
+    }
+
+    private static ChannelInfo? GetEpgChannelFromTunerChannel(
+        NameValuePair[] mappings,
+        ChannelInfo tunerChannel,
+        EpgChannelData epgChannelData)
+    {
+        if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
+        {
+            var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
+            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
+            {
+                mappedTunerChannelId = tunerChannel.Id;
+            }
+
+            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
+            if (channel is not null)
+            {
+                return channel;
+            }
+        }
+
+        if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
+        {
+            var tunerChannelId = tunerChannel.TunerChannelId;
+            if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
+            {
+                tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
+            }
+
+            var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
+            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
+            {
+                mappedTunerChannelId = tunerChannelId;
+            }
+
+            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
+            if (channel is not null)
+            {
+                return channel;
+            }
+        }
+
+        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
+        {
+            var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
+            if (string.IsNullOrWhiteSpace(tunerChannelNumber))
+            {
+                tunerChannelNumber = tunerChannel.Number;
+            }
+
+            var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
+            if (channel is not null)
+            {
+                return channel;
+            }
+        }
+
+        if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
+        {
+            var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
+
+            var channel = epgChannelData.GetChannelByName(normalizedName);
+            if (channel is not null)
+            {
+                return channel;
+            }
+        }
+
+        return null;
+    }
+
+    private static TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, IList<ChannelInfo> providerChannels)
+    {
+        var result = new TunerChannelMapping
+        {
+            Name = tunerChannel.Name,
+            Id = tunerChannel.Id
+        };
+
+        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
+        {
+            result.Name = tunerChannel.Number + " " + result.Name;
+        }
+
+        var providerChannel = GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(providerChannels));
+        if (providerChannel is not null)
+        {
+            result.ProviderChannelName = providerChannel.Name;
+            result.ProviderChannelId = providerChannel.Id;
+        }
+
+        return result;
+    }
+
+    private async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo info, CancellationToken cancellationToken)
+    {
+        var channels = new List<ChannelInfo>();
+        foreach (var hostInstance in _tunerHostManager.TunerHosts)
+        {
+            try
+            {
+                var tunerChannels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
+
+                channels.AddRange(tunerChannels.Where(channel => IsListingProviderEnabledForTuner(info, channel.TunerHostId)));
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error getting channels");
+            }
+        }
+
+        return channels;
+    }
+
+    private IListingsProvider GetProvider(string? providerType)
+        => _listingsProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase))
+           ?? throw new ResourceNotFoundException($"Couldn't find provider of type {providerType}");
+}

+ 1 - 167
src/Jellyfin.LiveTv/LiveTvManager.cs

@@ -6,14 +6,12 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
-using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using Jellyfin.LiveTv.Configuration;
-using Jellyfin.LiveTv.Guide;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
@@ -27,7 +25,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.LiveTv
@@ -43,12 +40,10 @@ namespace Jellyfin.LiveTv
         private readonly IDtoService _dtoService;
         private readonly IUserDataManager _userDataManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly ITaskManager _taskManager;
         private readonly ILocalizationManager _localization;
         private readonly IChannelManager _channelManager;
         private readonly LiveTvDtoService _tvDtoService;
         private readonly ILiveTvService[] _services;
-        private readonly IListingsProvider[] _listingProviders;
 
         public LiveTvManager(
             IServerConfigurationManager config,
@@ -57,25 +52,21 @@ namespace Jellyfin.LiveTv
             IDtoService dtoService,
             IUserManager userManager,
             ILibraryManager libraryManager,
-            ITaskManager taskManager,
             ILocalizationManager localization,
             IChannelManager channelManager,
             LiveTvDtoService liveTvDtoService,
-            IEnumerable<ILiveTvService> services,
-            IEnumerable<IListingsProvider> listingProviders)
+            IEnumerable<ILiveTvService> services)
         {
             _config = config;
             _logger = logger;
             _userManager = userManager;
             _libraryManager = libraryManager;
-            _taskManager = taskManager;
             _localization = localization;
             _dtoService = dtoService;
             _userDataManager = userDataManager;
             _channelManager = channelManager;
             _tvDtoService = liveTvDtoService;
             _services = services.ToArray();
-            _listingProviders = listingProviders.ToArray();
 
             var defaultService = _services.OfType<EmbyTV.EmbyTV>().First();
             defaultService.TimerCreated += OnEmbyTvTimerCreated;
@@ -96,8 +87,6 @@ namespace Jellyfin.LiveTv
         /// <value>The services.</value>
         public IReadOnlyList<ILiveTvService> Services => _services;
 
-        public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
-
         public string GetEmbyTvActiveRecordingPath(string id)
         {
             return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
@@ -1465,161 +1454,6 @@ namespace Jellyfin.LiveTv
             return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
         }
 
-        public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
-        {
-            // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
-            // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
-            info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
-
-            var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
-
-            if (provider is null)
-            {
-                throw new ResourceNotFoundException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "Couldn't find provider of type: '{0}'",
-                        info.Type));
-            }
-
-            await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
-
-            var config = _config.GetLiveTvConfiguration();
-
-            var list = config.ListingProviders.ToList();
-            int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
-
-            if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
-            {
-                info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-                list.Add(info);
-                config.ListingProviders = list.ToArray();
-            }
-            else
-            {
-                config.ListingProviders[index] = info;
-            }
-
-            _config.SaveConfiguration("livetv", config);
-
-            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
-
-            return info;
-        }
-
-        public void DeleteListingsProvider(string id)
-        {
-            var config = _config.GetLiveTvConfiguration();
-
-            config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
-
-            _config.SaveConfiguration("livetv", config);
-            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
-        }
-
-        public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
-        {
-            var config = _config.GetLiveTvConfiguration();
-
-            var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
-            listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
-
-            if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
-            {
-                var list = listingsProviderInfo.ChannelMappings.ToList();
-                list.Add(new NameValuePair
-                {
-                    Name = tunerChannelNumber,
-                    Value = providerChannelNumber
-                });
-                listingsProviderInfo.ChannelMappings = list.ToArray();
-            }
-
-            _config.SaveConfiguration("livetv", config);
-
-            var tunerChannels = await GetChannelsForListingsProvider(providerId, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-            var providerChannels = await GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
-                     .ConfigureAwait(false);
-
-            var mappings = listingsProviderInfo.ChannelMappings;
-
-            var tunerChannelMappings =
-                tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
-
-            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
-
-            return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
-        }
-
-        public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
-        {
-            var result = new TunerChannelMapping
-            {
-                Name = tunerChannel.Name,
-                Id = tunerChannel.Id
-            };
-
-            if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
-            {
-                result.Name = tunerChannel.Number + " " + result.Name;
-            }
-
-            var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
-
-            if (providerChannel is not null)
-            {
-                result.ProviderChannelName = providerChannel.Name;
-                result.ProviderChannelId = providerChannel.Id;
-            }
-
-            return result;
-        }
-
-        public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location)
-        {
-            var config = _config.GetLiveTvConfiguration();
-
-            if (string.IsNullOrWhiteSpace(providerId))
-            {
-                var provider = _listingProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase));
-
-                if (provider is null)
-                {
-                    throw new ResourceNotFoundException();
-                }
-
-                return provider.GetLineups(null, country, location);
-            }
-            else
-            {
-                var info = config.ListingProviders.FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase));
-
-                var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
-
-                if (provider is null)
-                {
-                    throw new ResourceNotFoundException();
-                }
-
-                return provider.GetLineups(info, country, location);
-            }
-        }
-
-        public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken)
-        {
-            var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
-            return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken);
-        }
-
-        public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken)
-        {
-            var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
-            var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase));
-            return provider.GetChannels(info, cancellationToken);
-        }
-
         /// <inheritdoc />
         public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
             => GetRecordingFoldersAsync(user, false);