< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Listings.XmlTvListingsProvider
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
Line coverage
62%
Covered lines: 88
Uncovered lines: 52
Coverable lines: 140
Total lines: 296
Line coverage: 62.8%
Branch coverage
48%
Covered branches: 40
Total branches: 82
Branch coverage: 48.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/17/2026 - 12:11:27 AM Line coverage: 75.8% (44/58) Branch coverage: 54.7% (23/42) Total lines: 2694/19/2026 - 12:14:27 AM Line coverage: 66.6% (86/129) Branch coverage: 56.7% (42/74) Total lines: 2715/6/2026 - 12:15:23 AM Line coverage: 62.8% (88/140) Branch coverage: 53.6% (44/82) Total lines: 2965/20/2026 - 12:15:44 AM Line coverage: 62.8% (88/140) Branch coverage: 48.7% (40/82) Total lines: 296 2/17/2026 - 12:11:27 AM Line coverage: 75.8% (44/58) Branch coverage: 54.7% (23/42) Total lines: 2694/19/2026 - 12:14:27 AM Line coverage: 66.6% (86/129) Branch coverage: 56.7% (42/74) Total lines: 2715/6/2026 - 12:15:23 AM Line coverage: 62.8% (88/140) Branch coverage: 53.6% (44/82) Total lines: 2965/20/2026 - 12:15:44 AM Line coverage: 62.8% (88/140) Branch coverage: 48.7% (40/82) Total lines: 296

Coverage delta

Coverage delta 10 -10

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
get_Name()100%210%
get_Type()100%210%
GetLanguage(...)50%2266.66%
GetXml()42.85%241462.96%
UnzipIfNeededAndCopy()40%151062.5%
GetProgramsAsync()50%2288.88%
GetProgramInfo(...)56%575085.71%
Validate(...)0%2040%
GetLineups()100%210%
GetChannels()100%210%

File(s)

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

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.IO;
 7using System.IO.Compression;
 8using System.Linq;
 9using System.Net.Http;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Jellyfin.Extensions;
 13using Jellyfin.XmlTv;
 14using Jellyfin.XmlTv.Entities;
 15using MediaBrowser.Common.Extensions;
 16using MediaBrowser.Common.Net;
 17using MediaBrowser.Controller.Configuration;
 18using MediaBrowser.Controller.LiveTv;
 19using MediaBrowser.Model.Dto;
 20using MediaBrowser.Model.IO;
 21using MediaBrowser.Model.LiveTv;
 22using Microsoft.Extensions.Logging;
 23
 24namespace Jellyfin.LiveTv.Listings
 25{
 26    public class XmlTvListingsProvider : IListingsProvider
 27    {
 028        private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1);
 29
 30        private readonly IServerConfigurationManager _config;
 31        private readonly IHttpClientFactory _httpClientFactory;
 32        private readonly ILogger<XmlTvListingsProvider> _logger;
 33
 34        public XmlTvListingsProvider(
 35            IServerConfigurationManager config,
 36            IHttpClientFactory httpClientFactory,
 37            ILogger<XmlTvListingsProvider> logger)
 38        {
 2539            _config = config;
 2540            _httpClientFactory = httpClientFactory;
 2541            _logger = logger;
 2542        }
 43
 044        public string Name => "XmlTV";
 45
 046        public string Type => "xmltv";
 47
 48        private string GetLanguage(ListingsProviderInfo info)
 49        {
 450            if (!string.IsNullOrWhiteSpace(info.PreferredLanguage))
 51            {
 052                return info.PreferredLanguage;
 53            }
 54
 455            return _config.Configuration.PreferredMetadataLanguage;
 56        }
 57
 58        private async Task<string> GetXml(ListingsProviderInfo info, CancellationToken cancellationToken)
 59        {
 460            _logger.LogInformation("xmltv path: {Path}", info.Path);
 61
 462            string cacheFilename = info.Id + ".xml";
 463            string cacheDir = Path.Join(_config.ApplicationPaths.CachePath, "xmltv");
 464            string cacheFile = Path.Join(cacheDir, cacheFilename);
 65
 466            if (File.Exists(cacheFile))
 67            {
 068                if (File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
 69                {
 070                    return cacheFile;
 71                }
 72
 073                File.Delete(cacheFile);
 74            }
 75            else
 76            {
 477                Directory.CreateDirectory(cacheDir);
 78            }
 79
 80            try
 81            {
 482                if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
 83                {
 284                    _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
 85
 286                    using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, 
 287                    var redirectedUrl = response.RequestMessage?.RequestUri?.ToString() ?? info.Path;
 288                    var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
 289                    await using (stream.ConfigureAwait(false))
 90                    {
 291                        return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).Configure
 92                    }
 093                }
 94                else
 95                {
 296                    var stream = AsyncFile.OpenRead(info.Path);
 297                    await using (stream.ConfigureAwait(false))
 98                    {
 299                        return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwai
 100                    }
 101                }
 0102            }
 0103            catch (Exception ex)
 104            {
 0105                _logger.LogError(ex, "Error downloading or processing XMLTV file from {Path}", info.Path);
 106
 0107                if (File.Exists(cacheFile))
 108                {
 0109                    File.Delete(cacheFile);
 110                }
 111
 0112                throw;
 113            }
 4114        }
 115
 116        private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToke
 117        {
 4118            var fileStream = new FileStream(
 4119                file,
 4120                FileMode.CreateNew,
 4121                FileAccess.Write,
 4122                FileShare.None,
 4123                IODefaults.FileStreamBufferSize,
 4124                FileOptions.Asynchronous);
 125
 4126            await using (fileStream.ConfigureAwait(false))
 127            {
 4128                if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCa
 4129                    Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gzip", StringComparison.OrdinalIgnore
 130                {
 131                    try
 132                    {
 0133                        using var reader = new GZipStream(stream, CompressionMode.Decompress);
 0134                        await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
 0135                    }
 0136                    catch (Exception ex)
 137                    {
 0138                        _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl);
 0139                    }
 140                }
 141                else
 142                {
 4143                    await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
 144                }
 145            }
 146
 4147            var fileInfo = new FileInfo(file);
 4148            if (!fileInfo.Exists || fileInfo.Length == 0)
 149            {
 0150                if (fileInfo.Exists)
 151                {
 0152                    File.Delete(file);
 153                }
 154
 0155                throw new InvalidOperationException("Downloaded XMLTV file is empty: " + originalUrl);
 156            }
 157
 4158            return file;
 4159        }
 160
 161        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTi
 162        {
 4163            if (string.IsNullOrWhiteSpace(channelId))
 164            {
 0165                throw new ArgumentNullException(nameof(channelId));
 166            }
 167
 4168            _logger.LogDebug("Getting xmltv programs for channel {Id}", channelId);
 169
 4170            string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
 4171            _logger.LogDebug("Opening XmlTvReader for {Path}", path);
 4172            var reader = new XmlTvReader(path, GetLanguage(info));
 173
 4174            return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken)
 4175                        .Select(p => GetProgramInfo(p, info));
 4176        }
 177
 178        private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
 179        {
 4180            string? episodeTitle = program.Episode?.Title;
 4181            var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
 4182            var imageUrl = program.Icons.FirstOrDefault()?.Source;
 4183            var rating = program.Ratings.FirstOrDefault()?.Value;
 4184            var starRating = program.StarRatings?.FirstOrDefault()?.StarRating;
 185
 4186            var programInfo = new ProgramInfo
 4187            {
 4188                ChannelId = program.ChannelId,
 4189                EndDate = program.EndDate.UtcDateTime,
 4190                EpisodeNumber = program.Episode?.Episode,
 4191                EpisodeTitle = episodeTitle,
 4192                Genres = programCategories,
 4193                StartDate = program.StartDate.UtcDateTime,
 4194                Name = program.Title,
 4195                Overview = program.Description,
 4196                ProductionYear = program.CopyrightDate?.Year,
 4197                SeasonNumber = program.Episode?.Series,
 4198                IsSeries = program.Episode?.Episode is not null,
 4199                IsRepeat = program.IsPreviouslyShown && !program.IsNew,
 4200                IsPremiere = program.Premiere is not null,
 4201                IsLive = program.IsLive,
 4202                IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase))
 4203                IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase
 4204                IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase))
 4205                IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCa
 4206                ImageUrl = string.IsNullOrEmpty(imageUrl) ? null : imageUrl,
 4207                HasImage = !string.IsNullOrEmpty(imageUrl),
 4208                OfficialRating = string.IsNullOrEmpty(rating) ? null : rating,
 4209                CommunityRating = starRating is null ? null : (float)starRating.Value,
 4210                SeriesId = program.Episode?.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.I
 4211            };
 212
 4213            if (string.IsNullOrWhiteSpace(program.ProgramId))
 214            {
 4215                string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty);
 216
 4217                if (programInfo.SeasonNumber.HasValue)
 218                {
 0219                    uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture);
 220                }
 221
 4222                if (programInfo.EpisodeNumber.HasValue)
 223                {
 0224                    uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture);
 225                }
 226
 4227                programInfo.ShowId = uniqueString.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 228
 229                // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skip
 4230                if (programInfo.IsSeries
 4231                    && !programInfo.IsRepeat
 4232                    && (programInfo.EpisodeNumber ?? 0) == 0)
 233                {
 0234                    programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
 235                }
 236            }
 237            else
 238            {
 0239                programInfo.ShowId = program.ProgramId;
 240            }
 241
 242            // Construct an id from the channel and start date
 4243            programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDa
 244
 4245            if (programInfo.IsMovie)
 246            {
 0247                programInfo.IsSeries = false;
 0248                programInfo.EpisodeNumber = null;
 0249                programInfo.EpisodeTitle = null;
 250            }
 251
 4252            return programInfo;
 253        }
 254
 255        public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
 256        {
 257            // Assume all urls are valid. check files for existence
 0258            if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path))
 259            {
 0260                throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path);
 261            }
 262
 0263            return Task.CompletedTask;
 264        }
 265
 266        public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
 267        {
 268            // In theory this should never be called because there is always only one lineup
 0269            string path = await GetXml(info, CancellationToken.None).ConfigureAwait(false);
 0270            _logger.LogDebug("Opening XmlTvReader for {Path}", path);
 0271            var reader = new XmlTvReader(path, GetLanguage(info));
 0272            IEnumerable<XmlTvChannel> results = reader.GetChannels();
 273
 274            // Should this method be async?
 0275            return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList();
 0276        }
 277
 278        public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
 279        {
 280            // In theory this should never be called because there is always only one lineup
 0281            string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
 0282            _logger.LogDebug("Opening XmlTvReader for {Path}", path);
 0283            var reader = new XmlTvReader(path, GetLanguage(info));
 0284            var results = reader.GetChannels();
 285
 286            // Should this method be async?
 0287            return results.Select(c => new ChannelInfo
 0288            {
 0289                Id = c.Id,
 0290                Name = c.DisplayName,
 0291                ImageUrl = string.IsNullOrEmpty(c.Icons.FirstOrDefault()?.Source) ? null : c.Icons.FirstOrDefault()!.Sou
 0292                Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
 0293            }).ToList();
 0294        }
 295    }
 296}