< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Listings.ListingsManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
Line coverage
7%
Covered lines: 17
Uncovered lines: 208
Coverable lines: 225
Total lines: 503
Line coverage: 7.5%
Branch coverage
3%
Covered branches: 3
Total branches: 92
Branch coverage: 3.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 15.5% (12/77) Branch coverage: 0% (0/40) Total lines: 4644/19/2026 - 12:14:27 AM Line coverage: 5.7% (12/209) Branch coverage: 0% (0/84) Total lines: 4645/6/2026 - 12:15:23 AM Line coverage: 7.5% (17/225) Branch coverage: 3.2% (3/92) Total lines: 503 1/23/2026 - 12:11:06 AM Line coverage: 15.5% (12/77) Branch coverage: 0% (0/40) Total lines: 4644/19/2026 - 12:14:27 AM Line coverage: 5.7% (12/209) Branch coverage: 0% (0/84) Total lines: 4645/6/2026 - 12:15:23 AM Line coverage: 7.5% (17/225) Branch coverage: 3.2% (3/92) Total lines: 503

Coverage delta

Coverage delta 10 -10

Metrics

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.IO;
 6using System.Linq;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Jellyfin.LiveTv.Configuration;
 10using Jellyfin.LiveTv.Guide;
 11using MediaBrowser.Common.Configuration;
 12using MediaBrowser.Common.Extensions;
 13using MediaBrowser.Controller.LiveTv;
 14using MediaBrowser.Model.Dto;
 15using MediaBrowser.Model.LiveTv;
 16using MediaBrowser.Model.Tasks;
 17using Microsoft.Extensions.Logging;
 18
 19namespace Jellyfin.LiveTv.Listings;
 20
 21/// <inheritdoc />
 22public class ListingsManager : IListingsManager
 23{
 24    private readonly ILogger<ListingsManager> _logger;
 25    private readonly IConfigurationManager _config;
 26    private readonly ITaskManager _taskManager;
 27    private readonly ITunerHostManager _tunerHostManager;
 28    private readonly IListingsProvider[] _listingsProviders;
 29
 2230    private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = new(StringComparer.OrdinalIgnoreCase);
 31
 32    /// <summary>
 33    /// Initializes a new instance of the <see cref="ListingsManager"/> class.
 34    /// </summary>
 35    /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
 36    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
 37    /// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
 38    /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
 39    /// <param name="listingsProviders">The <see cref="IListingsProvider"/>.</param>
 40    public ListingsManager(
 41        ILogger<ListingsManager> logger,
 42        IConfigurationManager config,
 43        ITaskManager taskManager,
 44        ITunerHostManager tunerHostManager,
 45        IEnumerable<IListingsProvider> listingsProviders)
 46    {
 2247        _logger = logger;
 2248        _config = config;
 2249        _taskManager = taskManager;
 2250        _tunerHostManager = tunerHostManager;
 2251        _listingsProviders = listingsProviders.ToArray();
 2252    }
 53
 54    /// <inheritdoc />
 55    public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool vali
 56    {
 057        ArgumentNullException.ThrowIfNull(info);
 58
 059        var provider = GetProvider(info.Type);
 060        await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
 61
 062        var config = _config.GetLiveTvConfiguration();
 63
 064        var list = config.ListingProviders;
 065        int index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
 66
 067        if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
 68        {
 069            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 070            config.ListingProviders = [..list, info];
 71        }
 72        else
 73        {
 074            config.ListingProviders[index] = info;
 75        }
 76
 077        _config.SaveConfiguration("livetv", config);
 78
 079        InvalidateListingsProviderCache(info.Id);
 80
 081        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 82
 083        return info;
 084    }
 85
 86    /// <inheritdoc />
 87    public void DeleteListingsProvider(string? id)
 88    {
 189        var config = _config.GetLiveTvConfiguration();
 90
 191        config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIg
 92
 193        _config.SaveConfiguration("livetv", config);
 94
 195        if (!string.IsNullOrEmpty(id))
 96        {
 197            InvalidateListingsProviderCache(id);
 98        }
 99
 1100        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 1101    }
 102
 103    /// <inheritdoc />
 104    public Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location
 105    {
 0106        if (string.IsNullOrWhiteSpace(providerId))
 107        {
 0108            return GetProvider(providerType).GetLineups(null, country, location);
 109        }
 110
 0111        var info = _config.GetLiveTvConfiguration().ListingProviders
 0112            .FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase))
 0113            ?? throw new ResourceNotFoundException();
 114
 0115        return GetProvider(info.Type).GetLineups(info, country, location);
 116    }
 117
 118    /// <inheritdoc />
 119    public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
 120        ChannelInfo channel,
 121        DateTime startDateUtc,
 122        DateTime endDateUtc,
 123        CancellationToken cancellationToken)
 124    {
 0125        ArgumentNullException.ThrowIfNull(channel);
 126
 0127        foreach (var (provider, providerInfo) in GetListingProviders())
 128        {
 0129            if (!IsListingProviderEnabledForTuner(providerInfo, channel.TunerHostId))
 130            {
 0131                _logger.LogDebug(
 0132                    "Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner
 0133                    channel.Number,
 0134                    channel.Name,
 0135                    provider.Name,
 0136                    providerInfo.ListingsId ?? string.Empty);
 0137                continue;
 138            }
 139
 0140            _logger.LogDebug(
 0141                "Getting programs for channel {0}-{1} from {2}-{3}",
 0142                channel.Number,
 0143                channel.Name,
 0144                provider.Name,
 0145                providerInfo.ListingsId ?? string.Empty);
 146
 0147            var epgChannels = await GetEpgChannels(provider, providerInfo, true, cancellationToken).ConfigureAwait(false
 148
 0149            var epgChannel = GetEpgChannelFromTunerChannel(providerInfo.ChannelMappings, channel, epgChannels);
 0150            if (epgChannel is null)
 151            {
 0152                _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel
 0153                continue;
 154            }
 155
 0156            var programs = (await provider
 0157                .GetProgramsAsync(providerInfo, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken).ConfigureAwa
 0158                .ToList();
 159
 160            // Replace the value that came from the provider with a normalized value
 0161            foreach (var program in programs)
 162            {
 0163                program.ChannelId = channel.Id;
 0164                program.Id += "_" + channel.Id;
 165            }
 166
 0167            if (programs.Count > 0)
 168            {
 0169                return programs;
 170            }
 0171        }
 172
 0173        return Enumerable.Empty<ProgramInfo>();
 0174    }
 175
 176    /// <inheritdoc />
 177    public async Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationT
 178    {
 0179        ArgumentNullException.ThrowIfNull(channels);
 180
 0181        foreach (var (provider, providerInfo) in GetListingProviders())
 182        {
 0183            var enabledChannels = channels
 0184                .Where(i => IsListingProviderEnabledForTuner(providerInfo, i.TunerHostId))
 0185                .ToList();
 186
 0187            if (enabledChannels.Count == 0)
 188            {
 189                continue;
 190            }
 191
 192            try
 193            {
 0194                await AddMetadata(provider, providerInfo, enabledChannels, enableCache, cancellationToken).ConfigureAwai
 0195            }
 0196            catch (NotSupportedException)
 197            {
 0198            }
 0199            catch (Exception ex)
 200            {
 0201                _logger.LogError(ex, "Error adding metadata");
 0202            }
 203        }
 0204    }
 205
 206    /// <inheritdoc />
 207    public async Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId)
 208    {
 0209        var listingsProviderInfo = _config.GetLiveTvConfiguration().ListingProviders
 0210            .First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
 211
 0212        var provider = GetProvider(listingsProviderInfo.Type);
 213
 0214        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
 0215            .ConfigureAwait(false);
 216
 0217        var providerChannels = await provider.GetChannels(listingsProviderInfo, default)
 0218            .ConfigureAwait(false);
 219
 0220        var mappings = listingsProviderInfo.ChannelMappings;
 221
 0222        return new ChannelMappingOptionsDto
 0223        {
 0224            TunerChannels = tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
 0225            ProviderChannels = providerChannels.Select(i => new NameIdPair
 0226            {
 0227                Name = i.Name,
 0228                Id = i.Id
 0229            }).ToList(),
 0230            Mappings = mappings,
 0231            ProviderName = provider.Name
 0232        };
 0233    }
 234
 235    /// <inheritdoc />
 236    public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string provid
 237    {
 0238        var config = _config.GetLiveTvConfiguration();
 239
 0240        var listingsProviderInfo = config.ListingProviders
 0241            .First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
 242
 0243        var channelMappingExists = listingsProviderInfo.ChannelMappings
 0244            .Any(pair => string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)
 0245                        && string.Equals(pair.Value, providerChannelNumber, StringComparison.OrdinalIgnoreCase));
 246
 0247        listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
 0248            .Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
 249
 0250        if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)
 0251            && !channelMappingExists)
 252        {
 0253            var newItem = new NameValuePair
 0254            {
 0255                Name = tunerChannelNumber,
 0256                Value = providerChannelNumber
 0257            };
 0258            listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem];
 259        }
 260
 0261        _config.SaveConfiguration("livetv", config);
 262
 0263        var tunerChannels = await GetChannelsForListingsProvider(listingsProviderInfo, CancellationToken.None)
 0264            .ConfigureAwait(false);
 265
 0266        var providerChannels = await GetProvider(listingsProviderInfo.Type).GetChannels(listingsProviderInfo, default)
 0267            .ConfigureAwait(false);
 268
 0269        var tunerChannelMappings = tunerChannels
 0270            .Select(i => GetTunerChannelMapping(i, listingsProviderInfo.ChannelMappings, providerChannels)).ToList();
 271
 0272        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 273
 0274        return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCas
 0275    }
 276
 277    private List<(IListingsProvider Provider, ListingsProviderInfo ProviderInfo)> GetListingProviders()
 0278        => _config.GetLiveTvConfiguration().ListingProviders
 0279            .Select(info => (
 0280                Provider: _listingsProviders.FirstOrDefault(l
 0281                    => string.Equals(l.Type, info.Type, StringComparison.OrdinalIgnoreCase)),
 0282                ProviderInfo: info))
 0283            .Where(i => i.Provider is not null)
 0284            .ToList()!; // Already filtered out null
 285
 286    private async Task AddMetadata(
 287        IListingsProvider provider,
 288        ListingsProviderInfo info,
 289        IEnumerable<ChannelInfo> tunerChannels,
 290        bool enableCache,
 291        CancellationToken cancellationToken)
 292    {
 0293        var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false);
 294
 0295        foreach (var tunerChannel in tunerChannels)
 296        {
 0297            var epgChannel = GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels);
 0298            if (epgChannel is null)
 299            {
 300                continue;
 301            }
 302
 0303            if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
 304            {
 0305                tunerChannel.ImageUrl = epgChannel.ImageUrl;
 306            }
 307        }
 0308    }
 309
 310    private static bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
 311    {
 0312        if (info.EnableAllTuners)
 313        {
 0314            return true;
 315        }
 316
 0317        ArgumentException.ThrowIfNullOrWhiteSpace(tunerHostId);
 318
 0319        return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
 320    }
 321
 322    private static string GetMappedChannel(string channelId, NameValuePair[] mappings)
 323    {
 0324        foreach (NameValuePair mapping in mappings)
 325        {
 0326            if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase))
 327            {
 0328                return mapping.Value;
 329            }
 330        }
 331
 0332        return channelId;
 333    }
 334
 335    private void InvalidateListingsProviderCache(string providerId)
 336    {
 337        // Clear in-memory EPG channel cache for this provider
 1338        _epgChannels.TryRemove(providerId, out _);
 339
 340        // Provider IDs are generated as Guid.NewGuid().ToString("N")
 341        // reject anything else so we never use untrusted input in a path or log entry.
 1342        if (!Guid.TryParseExact(providerId, "N", out var providerGuid))
 343        {
 1344            return;
 345        }
 346
 347        // Delete the cached XMLTV file so a fresh copy is downloaded
 0348        var cachePath = _config.CommonApplicationPaths?.CachePath;
 0349        if (!string.IsNullOrEmpty(cachePath))
 350        {
 0351            var safeId = providerGuid.ToString("N", CultureInfo.InvariantCulture);
 0352            var xmltvCacheFile = Path.Combine(cachePath, "xmltv", safeId + ".xml");
 353            try
 354            {
 0355                File.Delete(xmltvCacheFile);
 0356            }
 0357            catch (IOException ex)
 358            {
 0359                _logger.LogWarning(ex, "Error deleting XMLTV cache file for provider {ProviderId}", safeId);
 0360            }
 361        }
 0362    }
 363
 364    private async Task<EpgChannelData> GetEpgChannels(
 365        IListingsProvider provider,
 366        ListingsProviderInfo info,
 367        bool enableCache,
 368        CancellationToken cancellationToken)
 369    {
 0370        if (enableCache && _epgChannels.TryGetValue(info.Id, out var result))
 371        {
 0372            return result;
 373        }
 374
 0375        var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
 0376        foreach (var channel in channels)
 377        {
 0378            _logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name,
 379        }
 380
 0381        result = new EpgChannelData(channels);
 0382        _epgChannels.AddOrUpdate(info.Id, result, (_, _) => result);
 383
 0384        return result;
 0385    }
 386
 387    private static ChannelInfo? GetEpgChannelFromTunerChannel(
 388        NameValuePair[] mappings,
 389        ChannelInfo tunerChannel,
 390        EpgChannelData epgChannelData)
 391    {
 0392        if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
 393        {
 0394            var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
 0395            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
 396            {
 0397                mappedTunerChannelId = tunerChannel.Id;
 398            }
 399
 0400            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
 0401            if (channel is not null)
 402            {
 0403                return channel;
 404            }
 405        }
 406
 0407        if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
 408        {
 0409            var tunerChannelId = tunerChannel.TunerChannelId;
 0410            if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
 411            {
 0412                tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.Ordi
 413            }
 414
 0415            var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
 0416            if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
 417            {
 0418                mappedTunerChannelId = tunerChannelId;
 419            }
 420
 0421            var channel = epgChannelData.GetChannelById(mappedTunerChannelId);
 0422            if (channel is not null)
 423            {
 0424                return channel;
 425            }
 426        }
 427
 0428        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
 429        {
 0430            var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings);
 0431            if (string.IsNullOrWhiteSpace(tunerChannelNumber))
 432            {
 0433                tunerChannelNumber = tunerChannel.Number;
 434            }
 435
 0436            var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber);
 0437            if (channel is not null)
 438            {
 0439                return channel;
 440            }
 441        }
 442
 0443        if (!string.IsNullOrWhiteSpace(tunerChannel.Name))
 444        {
 0445            var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name);
 446
 0447            var channel = epgChannelData.GetChannelByName(normalizedName);
 0448            if (channel is not null)
 449            {
 0450                return channel;
 451            }
 452        }
 453
 0454        return null;
 455    }
 456
 457    private static TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, IList<
 458    {
 0459        var result = new TunerChannelMapping
 0460        {
 0461            Name = tunerChannel.Name,
 0462            Id = tunerChannel.Id
 0463        };
 464
 0465        if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
 466        {
 0467            result.Name = tunerChannel.Number + " " + result.Name;
 468        }
 469
 0470        var providerChannel = GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(providerChannels)
 0471        if (providerChannel is not null)
 472        {
 0473            result.ProviderChannelName = providerChannel.Name;
 0474            result.ProviderChannelId = providerChannel.Id;
 475        }
 476
 0477        return result;
 478    }
 479
 480    private async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo info, CancellationToken ca
 481    {
 0482        var channels = new List<ChannelInfo>();
 0483        foreach (var hostInstance in _tunerHostManager.TunerHosts)
 484        {
 485            try
 486            {
 0487                var tunerChannels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
 488
 0489                channels.AddRange(tunerChannels.Where(channel => IsListingProviderEnabledForTuner(info, channel.TunerHos
 0490            }
 0491            catch (Exception ex)
 492            {
 0493                _logger.LogError(ex, "Error getting channels");
 0494            }
 495        }
 496
 0497        return channels;
 0498    }
 499
 500    private IListingsProvider GetProvider(string? providerType)
 0501        => _listingsProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase
 0502           ?? throw new ResourceNotFoundException($"Couldn't find provider of type {providerType}");
 503}