< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.TunerHosts.BaseTunerHost
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs
Line coverage
3%
Covered lines: 3
Uncovered lines: 89
Coverable lines: 92
Total lines: 232
Line coverage: 3.2%
Branch coverage
0%
Covered branches: 0
Total branches: 44
Branch coverage: 0%
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: 33.3% (3/9) Total lines: 2324/19/2026 - 12:14:27 AM Line coverage: 3.2% (3/92) Branch coverage: 0% (0/44) Total lines: 232 4/19/2026 - 12:14:27 AM Line coverage: 3.2% (3/92) Branch coverage: 0% (0/44) Total lines: 232

Coverage delta

Coverage delta 31 -31

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_IsSupported()100%11100%
get_ChannelIdPrefix()100%210%
GetChannels()0%110100%
GetTunerHosts()100%210%
GetChannels()0%156120%
GetChannelStreamMediaSources()0%7280%
GetChannelStream()0%210140%
IsValidChannelId(...)100%210%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Concurrent;
 7using System.Collections.Generic;
 8using System.IO;
 9using System.Linq;
 10using System.Text.Json;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using Jellyfin.LiveTv.Configuration;
 14using MediaBrowser.Controller.Configuration;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.LiveTv;
 17using MediaBrowser.Model.Dto;
 18using MediaBrowser.Model.IO;
 19using MediaBrowser.Model.LiveTv;
 20using Microsoft.Extensions.Logging;
 21
 22namespace Jellyfin.LiveTv.TunerHosts
 23{
 24    public abstract class BaseTunerHost
 25    {
 26        private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache;
 27
 28        protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSyste
 29        {
 30            Config = config;
 31            Logger = logger;
 32            FileSystem = fileSystem;
 4933            _cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
 4934        }
 35
 36        protected IServerConfigurationManager Config { get; }
 37
 38        protected ILogger<BaseTunerHost> Logger { get; }
 39
 40        protected IFileSystem FileSystem { get; }
 41
 4242        public virtual bool IsSupported => true;
 43
 44        public abstract string Type { get; }
 45
 046        protected virtual string ChannelIdPrefix => Type + "_";
 47
 48        protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellati
 49
 50        public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancel
 51        {
 052            var key = tuner.Id;
 53
 054            if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
 55            {
 056                return cache;
 57            }
 58
 059            var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
 60            // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
 61
 062            if (!string.IsNullOrEmpty(key) && list.Count > 0)
 63            {
 064                _cache[key] = list;
 65            }
 66
 067            return list;
 068        }
 69
 70        protected virtual IList<TunerHostInfo> GetTunerHosts()
 71        {
 072            return Config.GetLiveTvConfiguration().TunerHosts
 073                .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
 074                .ToList();
 75        }
 76
 77        public async Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken)
 78        {
 079            var list = new List<ChannelInfo>();
 80
 081            var hosts = GetTunerHosts();
 82
 083            foreach (var host in hosts)
 84            {
 085                var channelCacheFile = Path.Combine(Config.ApplicationPaths.CachePath, host.Id + "_channels");
 86
 87                try
 88                {
 089                    var channels = await GetChannels(host, enableCache, cancellationToken).ConfigureAwait(false);
 090                    var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.Ordi
 91
 092                    list.AddRange(newChannels);
 93
 094                    if (!enableCache)
 95                    {
 96                        try
 97                        {
 098                            Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
 099                            var writeStream = AsyncFile.OpenWrite(channelCacheFile);
 0100                            await using (writeStream.ConfigureAwait(false))
 101                            {
 0102                                await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellati
 103                            }
 0104                        }
 0105                        catch (IOException)
 106                        {
 0107                        }
 108                    }
 0109                }
 0110                catch (Exception ex)
 111                {
 0112                    Logger.LogError(ex, "Error getting channel list");
 113
 0114                    if (enableCache)
 115                    {
 116                        try
 117                        {
 0118                            var readStream = AsyncFile.OpenRead(channelCacheFile);
 0119                            await using (readStream.ConfigureAwait(false))
 120                            {
 0121                                var channels = await JsonSerializer
 0122                                    .DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToke
 0123                                    .ConfigureAwait(false);
 0124                                list.AddRange(channels);
 125                            }
 0126                        }
 0127                        catch (IOException)
 128                        {
 0129                        }
 130                    }
 131                }
 0132            }
 133
 0134            return list;
 0135        }
 136
 137        protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo cha
 138
 139        public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancel
 140        {
 0141            ArgumentException.ThrowIfNullOrEmpty(channelId);
 142
 0143            if (IsValidChannelId(channelId))
 144            {
 0145                var hosts = GetTunerHosts();
 146
 0147                foreach (var host in hosts)
 148                {
 149                    try
 150                    {
 0151                        var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
 0152                        var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.O
 153
 0154                        if (channelInfo is not null)
 155                        {
 0156                            return await GetChannelStreamMediaSources(host, channelInfo, cancellationToken).ConfigureAwa
 157                        }
 0158                    }
 0159                    catch (Exception ex)
 160                    {
 0161                        Logger.LogError(ex, "Error getting channels");
 0162                    }
 0163                }
 164            }
 165
 0166            return new List<MediaSourceInfo>();
 0167        }
 168
 169        protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string strea
 170
 171        public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, IList<ILiveStream> currentLiv
 172        {
 0173            ArgumentException.ThrowIfNullOrEmpty(channelId);
 174
 0175            if (!IsValidChannelId(channelId))
 176            {
 0177                throw new FileNotFoundException();
 178            }
 179
 0180            var hosts = GetTunerHosts();
 181
 0182            var hostsWithChannel = new List<Tuple<TunerHostInfo, ChannelInfo>>();
 183
 0184            foreach (var host in hosts)
 185            {
 186                try
 187                {
 0188                    var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
 0189                    var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.Ordin
 190
 0191                    if (channelInfo is not null)
 192                    {
 0193                        hostsWithChannel.Add(new Tuple<TunerHostInfo, ChannelInfo>(host, channelInfo));
 194                    }
 0195                }
 0196                catch (Exception ex)
 197                {
 0198                    Logger.LogError(ex, "Error getting channels");
 0199                }
 0200            }
 201
 0202            foreach (var hostTuple in hostsWithChannel)
 203            {
 0204                var host = hostTuple.Item1;
 0205                var channelInfo = hostTuple.Item2;
 206
 207                try
 208                {
 0209                    var liveStream = await GetChannelStream(host, channelInfo, streamId, currentLiveStreams, cancellatio
 0210                    var startTime = DateTime.UtcNow;
 0211                    await liveStream.Open(cancellationToken).ConfigureAwait(false);
 0212                    var endTime = DateTime.UtcNow;
 0213                    Logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
 0214                    return liveStream;
 215                }
 0216                catch (Exception ex)
 217                {
 0218                    Logger.LogError(ex, "Error opening tuner");
 0219                }
 220            }
 221
 0222            throw new LiveTvConflictException("Unable to find host to play channel");
 0223        }
 224
 225        protected virtual bool IsValidChannelId(string channelId)
 226        {
 0227            ArgumentException.ThrowIfNullOrEmpty(channelId);
 228
 0229            return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
 230        }
 231    }
 232}