< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Listings.ListingsManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
Line coverage
9%
Covered lines: 7
Uncovered lines: 70
Coverable lines: 77
Total lines: 459
Line coverage: 9%
Branch coverage
0%
Covered branches: 0
Total branches: 40
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
DeleteListingsProvider(...)100%210%
GetLineups(...)0%2040%
GetListingProviders()100%210%
IsListingProviderEnabledForTuner(...)0%620%
GetMappedChannel(...)0%2040%
GetEpgChannelFromTunerChannel(...)0%600240%
GetTunerChannelMapping(...)0%2040%
GetProvider(...)0%620%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/ListingsManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Globalization;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.LiveTv.Configuration;
 9using Jellyfin.LiveTv.Guide;
 10using MediaBrowser.Common.Configuration;
 11using MediaBrowser.Common.Extensions;
 12using MediaBrowser.Controller.LiveTv;
 13using MediaBrowser.Model.Dto;
 14using MediaBrowser.Model.LiveTv;
 15using MediaBrowser.Model.Tasks;
 16using Microsoft.Extensions.Logging;
 17
 18namespace Jellyfin.LiveTv.Listings;
 19
 20/// <inheritdoc />
 21public class ListingsManager : IListingsManager
 22{
 23    private readonly ILogger<ListingsManager> _logger;
 24    private readonly IConfigurationManager _config;
 25    private readonly ITaskManager _taskManager;
 26    private readonly ITunerHostManager _tunerHostManager;
 27    private readonly IListingsProvider[] _listingsProviders;
 28
 2229    private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = new(StringComparer.OrdinalIgnoreCase);
 30
 31    /// <summary>
 32    /// Initializes a new instance of the <see cref="ListingsManager"/> class.
 33    /// </summary>
 34    /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
 35    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
 36    /// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
 37    /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
 38    /// <param name="listingsProviders">The <see cref="IListingsProvider"/>.</param>
 39    public ListingsManager(
 40        ILogger<ListingsManager> logger,
 41        IConfigurationManager config,
 42        ITaskManager taskManager,
 43        ITunerHostManager tunerHostManager,
 44        IEnumerable<IListingsProvider> listingsProviders)
 45    {
 2246        _logger = logger;
 2247        _config = config;
 2248        _taskManager = taskManager;
 2249        _tunerHostManager = tunerHostManager;
 2250        _listingsProviders = listingsProviders.ToArray();
 2251    }
 52
 53    /// <inheritdoc />
 54    public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool vali
 55    {
 56        ArgumentNullException.ThrowIfNull(info);
 57
 58        var provider = GetProvider(info.Type);
 59        await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
 60
 61        var config = _config.GetLiveTvConfiguration();
 62
 63        var list = config.ListingProviders;
 64        int index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
 65
 66        if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
 67        {
 68            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 69            config.ListingProviders = [..list, info];
 70        }
 71        else
 72        {
 73            config.ListingProviders[index] = info;
 74        }
 75
 76        _config.SaveConfiguration("livetv", config);
 77        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 78
 79        return info;
 80    }
 81
 82    /// <inheritdoc />
 83    public void DeleteListingsProvider(string? id)
 84    {
 085        var config = _config.GetLiveTvConfiguration();
 86
 087        config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIg
 88
 089        _config.SaveConfiguration("livetv", config);
 090        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 091    }
 92
 93    /// <inheritdoc />
 94    public Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location
 95    {
 096        if (string.IsNullOrWhiteSpace(providerId))
 97        {
 098            return GetProvider(providerType).GetLineups(null, country, location);
 99        }
 100
 0101        var info = _config.GetLiveTvConfiguration().ListingProviders
 0102            .FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase))
 0103            ?? throw new ResourceNotFoundException();
 104
 0105        return GetProvider(info.Type).GetLineups(info, country, location);
 106    }
 107
 108    /// <inheritdoc />
 109    public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
 110        ChannelInfo channel,
 111        DateTime startDateUtc,
 112        DateTime endDateUtc,
 113        CancellationToken cancellationToken)
 114    {
 115        ArgumentNullException.ThrowIfNull(channel);
 116
 117        foreach (var (provider, providerInfo) in GetListingProviders())
 118        {
 119            if (!IsListingProviderEnabledForTuner(providerInfo, channel.TunerHostId))
 120            {
 121                _logger.LogDebug(
 122                    "Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner
 123                    channel.Number,
 124                    channel.Name,
 125                    provider.Name,
 126                    providerInfo.ListingsId ?? string.Empty);
 127                continue;
 128            }
 129
 130            _logger.LogDebug(
 131                "Getting programs for channel {0}-{1} from {2}-{3}",
 132                channel.Number,
 133                channel.Name,
 134                provider.Name,
 135                providerInfo.ListingsId ?? string.Empty);
 136
 137            var epgChannels = await GetEpgChannels(provider, providerInfo, true, cancellationToken).ConfigureAwait(false
 138
 139            var epgChannel = GetEpgChannelFromTunerChannel(providerInfo.ChannelMappings, channel, epgChannels);
 140            if (epgChannel is null)
 141            {
 142                _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel
 143                continue;
 144            }
 145
 146            var programs = (await provider
 147                .GetProgramsAsync(providerInfo, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken).ConfigureAwa
 148                .ToList();
 149
 150            // Replace the value that came from the provider with a normalized value
 151            foreach (var program in programs)
 152            {
 153                program.ChannelId = channel.Id;
 154                program.Id += "_" + channel.Id;
 155            }
 156
 157            if (programs.Count > 0)
 158            {
 159                return programs;
 160            }
 161        }
 162
 163        return Enumerable.Empty<ProgramInfo>();
 164    }
 165
 166    /// <inheritdoc />
 167    public async Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationT
 168    {
 169        ArgumentNullException.ThrowIfNull(channels);
 170
 171        foreach (var (provider, providerInfo) in GetListingProviders())
 172        {
 173            var enabledChannels = channels
 174                .Where(i => IsListingProviderEnabledForTuner(providerInfo, i.TunerHostId))
 175                .ToList();
 176
 177            if (enabledChannels.Count == 0)
 178            {
 179                continue;
 180            }
 181
 182            try
 183            {
 184                await AddMetadata(provider, providerInfo, enabledChannels, enableCache, cancellationToken).ConfigureAwai
 185            }
 186            catch (NotSupportedException)
 187            {
 188            }
 189            catch (Exception ex)
 190            {
 191                _logger.LogError(ex, "Error adding metadata");
 192            }
 193        }
 194    }
 195
 196    /// <inheritdoc />
 197    public async Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId)
 198    {
 199        var listingsProviderInfo = _config.GetLiveTvConfiguration().ListingProviders
 200            .First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
 201
 202        var provider = GetProvider(listingsProviderInfo.Type);
 203
 204        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
 205            .ConfigureAwait(false);
 206
 207        var providerChannels = await provider.GetChannels(listingsProviderInfo, default)
 208            .ConfigureAwait(false);
 209
 210        var mappings = listingsProviderInfo.ChannelMappings;
 211
 212        return new ChannelMappingOptionsDto
 213        {
 214            TunerChannels = tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
 215            ProviderChannels = providerChannels.Select(i => new NameIdPair
 216            {
 217                Name = i.Name,
 218                Id = i.Id
 219            }).ToList(),
 220            Mappings = mappings,
 221            ProviderName = provider.Name
 222        };
 223    }
 224
 225    /// <inheritdoc />
 226    public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string provid
 227    {
 228        var config = _config.GetLiveTvConfiguration();
 229
 230        var listingsProviderInfo = config.ListingProviders
 231            .First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
 232
 233        listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
 234            .Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
 235
 236        if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
 237        {
 238            var newItem = new NameValuePair
 239            {
 240                Name = tunerChannelNumber,
 241                Value = providerChannelNumber
 242            };
 243            listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem];
 244        }
 245
 246        _config.SaveConfiguration("livetv", config);
 247
 248        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
 249            .ConfigureAwait(false);
 250
 251        var providerChannels = await GetProvider(listingsProviderInfo.Type).GetChannels(listingsProviderInfo, default)
 252            .ConfigureAwait(false);
 253
 254        var tunerChannelMappings = tunerChannels
 255            .Select(i => GetTunerChannelMapping(i, listingsProviderInfo.ChannelMappings, providerChannels)).ToList();
 256
 257        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 258
 259        return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCas
 260    }
 261
 262    private List<(IListingsProvider Provider, ListingsProviderInfo ProviderInfo)> GetListingProviders()
 0263        => _config.GetLiveTvConfiguration().ListingProviders
 0264            .Select(info => (
 0265                Provider: _listingsProviders.FirstOrDefault(l
 0266                    => string.Equals(l.Type, info.Type, StringComparison.OrdinalIgnoreCase)),
 0267                ProviderInfo: info))
 0268            .Where(i => i.Provider is not null)
 0269            .ToList()!; // Already filtered out null
 270
 271    private async Task AddMetadata(
 272        IListingsProvider provider,
 273        ListingsProviderInfo info,
 274        IEnumerable<ChannelInfo> tunerChannels,
 275        bool enableCache,
 276        CancellationToken cancellationToken)
 277    {
 278        var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
 279
 280        foreach (var tunerChannel in tunerChannels)
 281        {
 282            var epgChannel = GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
 283            if (epgChannel is null)
 284            {
 285                continue;
 286            }
 287
 288            if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
 289            {
 290                tunerChannel.ImageUrl = epgChannel.ImageUrl;
 291            }
 292        }
 293    }
 294
 295    private static bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
 296    {
 0297        if (info.EnableAllTuners)
 298        {
 0299            return true;
 300        }
 301
 0302        ArgumentException.ThrowIfNullOrWhiteSpace(tunerHostId);
 303
 0304        return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
 305    }
 306
 307    private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
 308    {
 0309        foreach (NameValuePair mapping in mappings)
 310        {
 0311            if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
 312            {
 0313                return mapping.Value;
 314            }
 315        }
 316
 0317        return channelId;
 318    }
 319
 320    private async Task<EpgChannelData> GetEpgChannels(
 321        IListingsProvider provider,
 322        ListingsProviderInfo info,
 323        bool enableCache,
 324        CancellationToken cancellationToken)
 325    {
 326        if (enableCache && _epgChannels.TryGetValue(info.Id, out var result))
 327        {
 328            return result;
 329        }
 330
 331        var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
 332        foreach (var channel in channels)
 333        {
 334            _logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name,
 335        }
 336
 337        result = new EpgChannelData(channels);
 338        _epgChannels.AddOrUpdate(info.Id, result, (_, _) => result);
 339
 340        return result;
 341    }
 342
 343    private static ChannelInfo? GetEpgChannelFromTunerChannel(
 344        NameValuePair[] mappings,
 345        ChannelInfo tunerChannel,
 346        EpgChannelData epgChannelData)
 347    {
 0348        if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
 349        {
 0350            var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
 0351            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
 352            {
 0353                mappedTunerChannelId = tunerChannel.Id;
 354            }
 355
 0356            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
 0357            if (channel is not null)
 358            {
 0359                return channel;
 360            }
 361        }
 362
 0363        if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
 364        {
 0365            var tunerChannelId = tunerChannel.TunerChannelId;
 0366            if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
 367            {
 0368                tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.Ordi
 369            }
 370
 0371            var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
 0372            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
 373            {
 0374                mappedTunerChannelId = tunerChannelId;
 375            }
 376
 0377            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
 0378            if (channel is not null)
 379            {
 0380                return channel;
 381            }
 382        }
 383
 0384        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
 385        {
 0386            var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
 0387            if (string.IsNullOrWhiteSpace(tunerChannelNumber))
 388            {
 0389                tunerChannelNumber = tunerChannel.Number;
 390            }
 391
 0392            var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
 0393            if (channel is not null)
 394            {
 0395                return channel;
 396            }
 397        }
 398
 0399        if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
 400        {
 0401            var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
 402
 0403            var channel = epgChannelData.GetChannelByName(normalizedName);
 0404            if (channel is not null)
 405            {
 0406                return channel;
 407            }
 408        }
 409
 0410        return null;
 411    }
 412
 413    private static TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, IList<
 414    {
 0415        var result = new TunerChannelMapping
 0416        {
 0417            Name = tunerChannel.Name,
 0418            Id = tunerChannel.Id
 0419        };
 420
 0421        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
 422        {
 0423            result.Name = tunerChannel.Number + " " + result.Name;
 424        }
 425
 0426        var providerChannel = GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(providerChannels)
 0427        if (providerChannel is not null)
 428        {
 0429            result.ProviderChannelName = providerChannel.Name;
 0430            result.ProviderChannelId = providerChannel.Id;
 431        }
 432
 0433        return result;
 434    }
 435
 436    private async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo info, CancellationToken ca
 437    {
 438        var channels = new List<ChannelInfo>();
 439        foreach (var hostInstance in _tunerHostManager.TunerHosts)
 440        {
 441            try
 442            {
 443                var tunerChannels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
 444
 445                channels.AddRange(tunerChannels.Where(channel => IsListingProviderEnabledForTuner(info, channel.TunerHos
 446            }
 447            catch (Exception ex)
 448            {
 449                _logger.LogError(ex, "Error getting channels");
 450            }
 451        }
 452
 453        return channels;
 454    }
 455
 456    private IListingsProvider GetProvider(string? providerType)
 0457        => _listingsProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase
 0458           ?? throw new ResourceNotFoundException($"Couldn't find provider of type {providerType}");
 459}