< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Listings.ListingsManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
Line coverage
15%
Covered lines: 12
Uncovered lines: 65
Coverable lines: 77
Total lines: 464
Line coverage: 15.5%
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%11100%
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    {
 185        var config = _config.GetLiveTvConfiguration();
 86
 187        config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIg
 88
 189        _config.SaveConfiguration("livetv", config);
 190        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 191    }
 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        var channelMappingExists = listingsProviderInfo.ChannelMappings
 234            .Any(pair => string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)
 235                        && string.Equals(pair.Value, providerChannelNumber, StringComparison.OrdinalIgnoreCase));
 236
 237        listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
 238            .Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
 239
 240        if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)
 241            && !channelMappingExists)
 242        {
 243            var newItem = new NameValuePair
 244            {
 245                Name = tunerChannelNumber,
 246                Value = providerChannelNumber
 247            };
 248            listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem];
 249        }
 250
 251        _config.SaveConfiguration("livetv", config);
 252
 253        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
 254            .ConfigureAwait(false);
 255
 256        var providerChannels = await GetProvider(listingsProviderInfo.Type).GetChannels(listingsProviderInfo, default)
 257            .ConfigureAwait(false);
 258
 259        var tunerChannelMappings = tunerChannels
 260            .Select(i => GetTunerChannelMapping(i, listingsProviderInfo.ChannelMappings, providerChannels)).ToList();
 261
 262        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 263
 264        return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCas
 265    }
 266
 267    private List<(IListingsProvider Provider, ListingsProviderInfo ProviderInfo)> GetListingProviders()
 0268        => _config.GetLiveTvConfiguration().ListingProviders
 0269            .Select(info => (
 0270                Provider: _listingsProviders.FirstOrDefault(l
 0271                    => string.Equals(l.Type, info.Type, StringComparison.OrdinalIgnoreCase)),
 0272                ProviderInfo: info))
 0273            .Where(i => i.Provider is not null)
 0274            .ToList()!; // Already filtered out null
 275
 276    private async Task AddMetadata(
 277        IListingsProvider provider,
 278        ListingsProviderInfo info,
 279        IEnumerable<ChannelInfo> tunerChannels,
 280        bool enableCache,
 281        CancellationToken cancellationToken)
 282    {
 283        var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
 284
 285        foreach (var tunerChannel in tunerChannels)
 286        {
 287            var epgChannel = GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
 288            if (epgChannel is null)
 289            {
 290                continue;
 291            }
 292
 293            if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
 294            {
 295                tunerChannel.ImageUrl = epgChannel.ImageUrl;
 296            }
 297        }
 298    }
 299
 300    private static bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
 301    {
 0302        if (info.EnableAllTuners)
 303        {
 0304            return true;
 305        }
 306
 0307        ArgumentException.ThrowIfNullOrWhiteSpace(tunerHostId);
 308
 0309        return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
 310    }
 311
 312    private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
 313    {
 0314        foreach (NameValuePair mapping in mappings)
 315        {
 0316            if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
 317            {
 0318                return mapping.Value;
 319            }
 320        }
 321
 0322        return channelId;
 323    }
 324
 325    private async Task<EpgChannelData> GetEpgChannels(
 326        IListingsProvider provider,
 327        ListingsProviderInfo info,
 328        bool enableCache,
 329        CancellationToken cancellationToken)
 330    {
 331        if (enableCache && _epgChannels.TryGetValue(info.Id, out var result))
 332        {
 333            return result;
 334        }
 335
 336        var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
 337        foreach (var channel in channels)
 338        {
 339            _logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name,
 340        }
 341
 342        result = new EpgChannelData(channels);
 343        _epgChannels.AddOrUpdate(info.Id, result, (_, _) => result);
 344
 345        return result;
 346    }
 347
 348    private static ChannelInfo? GetEpgChannelFromTunerChannel(
 349        NameValuePair[] mappings,
 350        ChannelInfo tunerChannel,
 351        EpgChannelData epgChannelData)
 352    {
 0353        if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
 354        {
 0355            var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
 0356            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
 357            {
 0358                mappedTunerChannelId = tunerChannel.Id;
 359            }
 360
 0361            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
 0362            if (channel is not null)
 363            {
 0364                return channel;
 365            }
 366        }
 367
 0368        if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
 369        {
 0370            var tunerChannelId = tunerChannel.TunerChannelId;
 0371            if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
 372            {
 0373                tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.Ordi
 374            }
 375
 0376            var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
 0377            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
 378            {
 0379                mappedTunerChannelId = tunerChannelId;
 380            }
 381
 0382            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
 0383            if (channel is not null)
 384            {
 0385                return channel;
 386            }
 387        }
 388
 0389        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
 390        {
 0391            var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
 0392            if (string.IsNullOrWhiteSpace(tunerChannelNumber))
 393            {
 0394                tunerChannelNumber = tunerChannel.Number;
 395            }
 396
 0397            var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
 0398            if (channel is not null)
 399            {
 0400                return channel;
 401            }
 402        }
 403
 0404        if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
 405        {
 0406            var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
 407
 0408            var channel = epgChannelData.GetChannelByName(normalizedName);
 0409            if (channel is not null)
 410            {
 0411                return channel;
 412            }
 413        }
 414
 0415        return null;
 416    }
 417
 418    private static TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, IList<
 419    {
 0420        var result = new TunerChannelMapping
 0421        {
 0422            Name = tunerChannel.Name,
 0423            Id = tunerChannel.Id
 0424        };
 425
 0426        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
 427        {
 0428            result.Name = tunerChannel.Number + " " + result.Name;
 429        }
 430
 0431        var providerChannel = GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(providerChannels)
 0432        if (providerChannel is not null)
 433        {
 0434            result.ProviderChannelName = providerChannel.Name;
 0435            result.ProviderChannelId = providerChannel.Id;
 436        }
 437
 0438        return result;
 439    }
 440
 441    private async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo info, CancellationToken ca
 442    {
 443        var channels = new List<ChannelInfo>();
 444        foreach (var hostInstance in _tunerHostManager.TunerHosts)
 445        {
 446            try
 447            {
 448                var tunerChannels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
 449
 450                channels.AddRange(tunerChannels.Where(channel => IsListingProviderEnabledForTuner(info, channel.TunerHos
 451            }
 452            catch (Exception ex)
 453            {
 454                _logger.LogError(ex, "Error getting channels");
 455            }
 456        }
 457
 458        return channels;
 459    }
 460
 461    private IListingsProvider GetProvider(string? providerType)
 0462        => _listingsProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase
 0463           ?? throw new ResourceNotFoundException($"Couldn't find provider of type {providerType}");
 464}