< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.TunerHosts.TunerHostManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs
Line coverage
47%
Covered lines: 35
Uncovered lines: 39
Coverable lines: 74
Total lines: 202
Line coverage: 47.2%
Branch coverage
46%
Covered branches: 14
Total branches: 30
Branch coverage: 46.6%
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: 45.4% (5/11) Total lines: 1744/19/2026 - 12:14:27 AM Line coverage: 57.3% (35/61) Branch coverage: 50% (14/28) Total lines: 1745/6/2026 - 12:15:23 AM Line coverage: 47.2% (35/74) Branch coverage: 46.6% (14/30) Total lines: 202 4/19/2026 - 12:14:27 AM Line coverage: 57.3% (35/61) Branch coverage: 50% (14/28) Total lines: 1745/6/2026 - 12:15:23 AM Line coverage: 47.2% (35/74) Branch coverage: 46.6% (14/30) Total lines: 202

Coverage delta

Coverage delta 12 -12

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_TunerHosts()100%210%
GetTunerHostTypes()100%210%
SaveTunerHost()80%101094.44%
DeleteTunerHost(...)0%620%
DiscoverTuners()0%7280%
ScanForTunerDeviceChanges()100%22100%
ScanForTunerDeviceChanges()50%9654.54%
DiscoverDevices()50%3250%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Text.Json;
 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.TunerHosts;
 20
 21/// <inheritdoc />
 22public class TunerHostManager : ITunerHostManager
 23{
 24    private const int TunerDiscoveryDurationMs = 3000;
 25
 26    private readonly ILogger<TunerHostManager> _logger;
 27    private readonly IConfigurationManager _config;
 28    private readonly ITaskManager _taskManager;
 29    private readonly ITunerHost[] _tunerHosts;
 30
 31    /// <summary>
 32    /// Initializes a new instance of the <see cref="TunerHostManager"/> class.
 33    /// </summary>
 34    /// <param name="logger">The <see cref="ILogger{T}"/>.</param>
 35    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
 36    /// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
 37    /// <param name="tunerHosts">The <see cref="IEnumerable{T}"/>.</param>
 38    public TunerHostManager(
 39        ILogger<TunerHostManager> logger,
 40        IConfigurationManager config,
 41        ITaskManager taskManager,
 42        IEnumerable<ITunerHost> tunerHosts)
 43    {
 2144        _logger = logger;
 2145        _config = config;
 2146        _taskManager = taskManager;
 2147        _tunerHosts = tunerHosts.Where(t => t.IsSupported).ToArray();
 2148    }
 49
 50    /// <inheritdoc />
 051    public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
 52
 53    /// <inheritdoc />
 54    public IEnumerable<NameIdPair> GetTunerHostTypes()
 055        => _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
 056        {
 057            Name = i.Name,
 058            Id = i.Type
 059        });
 60
 61    /// <inheritdoc />
 62    public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
 63    {
 364        info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info))!;
 65
 366        var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCa
 67
 368        if (provider is null)
 69        {
 170            throw new ResourceNotFoundException();
 71        }
 72
 273        if (provider is IConfigurableTunerHost configurable)
 74        {
 275            await configurable.Validate(info).ConfigureAwait(false);
 76        }
 77
 178        var config = _config.GetLiveTvConfiguration();
 79
 180        var list = config.TunerHosts;
 181        var index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
 82
 183        if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
 84        {
 185            info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 186            config.TunerHosts = [..list, info];
 87        }
 88        else
 89        {
 090            config.TunerHosts[index] = info;
 91        }
 92
 193        _config.SaveConfiguration("livetv", config);
 94
 195        if (dataSourceChanged)
 96        {
 197            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 98        }
 99
 1100        return info;
 1101    }
 102
 103    /// <inheritdoc />
 104    public void DeleteTunerHost(string? id)
 105    {
 0106        var config = _config.GetLiveTvConfiguration();
 0107        config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).T
 0108        _config.SaveConfiguration("livetv", config);
 109
 110        // Clean up the disk cache file for this tuner.
 111        // Tuner IDs are generated as Guid.NewGuid().ToString("N")
 112        // reject anything else so we never use untrusted input in a path or log entry
 0113        if (Guid.TryParseExact(id, "N", out var tunerGuid))
 114        {
 0115            var safeId = tunerGuid.ToString("N", CultureInfo.InvariantCulture);
 0116            var channelCacheFile = Path.Combine(_config.CommonApplicationPaths.CachePath, safeId + "_channels");
 117            try
 118            {
 0119                File.Delete(channelCacheFile);
 0120            }
 0121            catch (IOException ex)
 122            {
 0123                _logger.LogWarning(ex, "Error deleting channel cache file for tuner {TunerId}", safeId);
 0124            }
 125        }
 126
 0127        _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 0128    }
 129
 130    /// <inheritdoc />
 131    public async IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly)
 132    {
 0133        var configuredDeviceIds = _config.GetLiveTvConfiguration().TunerHosts
 0134            .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
 0135            .Select(i => i.DeviceId)
 0136            .ToList();
 137
 0138        foreach (var host in _tunerHosts)
 139        {
 0140            var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, CancellationToken.None).Config
 0141            foreach (var tuner in discoveredDevices)
 142            {
 0143                if (!newDevicesOnly || !configuredDeviceIds.Contains(tuner.DeviceId, StringComparer.OrdinalIgnoreCase))
 144                {
 0145                    yield return tuner;
 146                }
 147            }
 148        }
 0149    }
 150
 151    /// <inheritdoc />
 152    public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
 153    {
 6154        foreach (var host in _tunerHosts)
 155        {
 2156            await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
 157        }
 1158    }
 159
 160    private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
 161    {
 2162        var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(
 163
 2164        var configuredDevices = _config.GetLiveTvConfiguration().TunerHosts
 2165            .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
 2166            .ToList();
 167
 4168        foreach (var device in discoveredDevices)
 169        {
 0170            var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, Stri
 171
 0172            if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.Ordina
 173            {
 0174                _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, dev
 175
 0176                configuredDevice.Url = device.Url;
 0177                await SaveTunerHost(configuredDevice).ConfigureAwait(false);
 178            }
 179        }
 2180    }
 181
 182    private async Task<IList<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken
 183    {
 184        try
 185        {
 2186            var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(fa
 187
 4188            foreach (var device in discoveredDevices)
 189            {
 0190                _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url);
 191            }
 192
 2193            return discoveredDevices;
 194        }
 0195        catch (Exception ex)
 196        {
 0197            _logger.LogError(ex, "Error discovering tuner devices");
 198
 0199            return Array.Empty<TunerHostInfo>();
 200        }
 2201    }
 202}