< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Listings.XmlTvListingsProvider
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
Line coverage
75%
Covered lines: 44
Uncovered lines: 14
Coverable lines: 58
Total lines: 267
Line coverage: 75.8%
Branch coverage
54%
Covered branches: 23
Total branches: 42
Branch coverage: 54.7%
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
.cctor()100%210%
.ctor(...)100%11100%
get_Name()100%210%
get_Type()100%210%
GetLanguage(...)50%2.15266.66%
GetProgramInfo(...)61.11%40.883684.44%
Validate(...)0%2040%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.IO.Compression;
 10using System.Linq;
 11using System.Net.Http;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using Jellyfin.Extensions;
 15using Jellyfin.XmlTv;
 16using Jellyfin.XmlTv.Entities;
 17using MediaBrowser.Common.Extensions;
 18using MediaBrowser.Common.Net;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.LiveTv;
 21using MediaBrowser.Model.Dto;
 22using MediaBrowser.Model.IO;
 23using MediaBrowser.Model.LiveTv;
 24using Microsoft.Extensions.Logging;
 25
 26namespace Jellyfin.LiveTv.Listings
 27{
 28    public class XmlTvListingsProvider : IListingsProvider
 29    {
 030        private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1);
 31
 32        private readonly IServerConfigurationManager _config;
 33        private readonly IHttpClientFactory _httpClientFactory;
 34        private readonly ILogger<XmlTvListingsProvider> _logger;
 35
 36        public XmlTvListingsProvider(
 37            IServerConfigurationManager config,
 38            IHttpClientFactory httpClientFactory,
 39            ILogger<XmlTvListingsProvider> logger)
 40        {
 2641            _config = config;
 2642            _httpClientFactory = httpClientFactory;
 2643            _logger = logger;
 2644        }
 45
 046        public string Name => "XmlTV";
 47
 048        public string Type => "xmltv";
 49
 50        private string GetLanguage(ListingsProviderInfo info)
 51        {
 452            if (!string.IsNullOrWhiteSpace(info.PreferredLanguage))
 53            {
 054                return info.PreferredLanguage;
 55            }
 56
 457            return _config.Configuration.PreferredMetadataLanguage;
 58        }
 59
 60        private async Task<string> GetXml(ListingsProviderInfo info, CancellationToken cancellationToken)
 61        {
 62            _logger.LogInformation("xmltv path: {Path}", info.Path);
 63
 64            string cacheFilename = info.Id + ".xml";
 65            string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
 66
 67            if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
 68            {
 69                return cacheFile;
 70            }
 71
 72            // Must check if file exists as parent directory may not exist.
 73            if (File.Exists(cacheFile))
 74            {
 75                File.Delete(cacheFile);
 76            }
 77            else
 78            {
 79                Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
 80            }
 81
 82            if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
 83            {
 84                _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
 85
 86                using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, canc
 87                var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
 88                await using (stream.ConfigureAwait(false))
 89                {
 90                    return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(fa
 91                }
 92            }
 93            else
 94            {
 95                var stream = AsyncFile.OpenRead(info.Path);
 96                await using (stream.ConfigureAwait(false))
 97                {
 98                    return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(fa
 99                }
 100            }
 101        }
 102
 103        private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToke
 104        {
 105            var fileStream = new FileStream(
 106                file,
 107                FileMode.CreateNew,
 108                FileAccess.Write,
 109                FileShare.None,
 110                IODefaults.FileStreamBufferSize,
 111                FileOptions.Asynchronous);
 112
 113            await using (fileStream.ConfigureAwait(false))
 114            {
 115                if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCa
 116                {
 117                    try
 118                    {
 119                        using var reader = new GZipStream(stream, CompressionMode.Decompress);
 120                        await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
 121                    }
 122                    catch (Exception ex)
 123                    {
 124                        _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl);
 125                    }
 126                }
 127                else
 128                {
 129                    await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
 130                }
 131
 132                return file;
 133            }
 134        }
 135
 136        public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTi
 137        {
 138            if (string.IsNullOrWhiteSpace(channelId))
 139            {
 140                throw new ArgumentNullException(nameof(channelId));
 141            }
 142
 143            _logger.LogDebug("Getting xmltv programs for channel {Id}", channelId);
 144
 145            string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
 146            _logger.LogDebug("Opening XmlTvReader for {Path}", path);
 147            var reader = new XmlTvReader(path, GetLanguage(info));
 148
 149            return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken)
 150                        .Select(p => GetProgramInfo(p, info));
 151        }
 152
 153        private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
 154        {
 4155            string episodeTitle = program.Episode.Title;
 4156            var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
 157
 4158            var programInfo = new ProgramInfo
 4159            {
 4160                ChannelId = program.ChannelId,
 4161                EndDate = program.EndDate.UtcDateTime,
 4162                EpisodeNumber = program.Episode.Episode,
 4163                EpisodeTitle = episodeTitle,
 4164                Genres = programCategories,
 4165                StartDate = program.StartDate.UtcDateTime,
 4166                Name = program.Title,
 4167                Overview = program.Description,
 4168                ProductionYear = program.CopyrightDate?.Year,
 4169                SeasonNumber = program.Episode.Series,
 4170                IsSeries = program.Episode.Episode is not null,
 4171                IsRepeat = program.IsPreviouslyShown && !program.IsNew,
 4172                IsPremiere = program.Premiere is not null,
 4173                IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase))
 4174                IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase
 4175                IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase))
 4176                IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCa
 4177                ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
 4178                HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
 4179                OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
 4180                CommunityRating = program.StarRating,
 4181                SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.In
 4182            };
 183
 4184            if (string.IsNullOrWhiteSpace(program.ProgramId))
 185            {
 4186                string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty);
 187
 4188                if (programInfo.SeasonNumber.HasValue)
 189                {
 0190                    uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture);
 191                }
 192
 4193                if (programInfo.EpisodeNumber.HasValue)
 194                {
 0195                    uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture);
 196                }
 197
 4198                programInfo.ShowId = uniqueString.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 199
 200                // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skip
 4201                if (programInfo.IsSeries
 4202                    && !programInfo.IsRepeat
 4203                    && (programInfo.EpisodeNumber ?? 0) == 0)
 204                {
 0205                    programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
 206                }
 207            }
 208            else
 209            {
 0210                programInfo.ShowId = program.ProgramId;
 211            }
 212
 213            // Construct an id from the channel and start date
 4214            programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDa
 215
 4216            if (programInfo.IsMovie)
 217            {
 0218                programInfo.IsSeries = false;
 0219                programInfo.EpisodeNumber = null;
 0220                programInfo.EpisodeTitle = null;
 221            }
 222
 4223            return programInfo;
 224        }
 225
 226        public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
 227        {
 228            // Assume all urls are valid. check files for existence
 0229            if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path))
 230            {
 0231                throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path);
 232            }
 233
 0234            return Task.CompletedTask;
 235        }
 236
 237        public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
 238        {
 239            // In theory this should never be called because there is always only one lineup
 240            string path = await GetXml(info, CancellationToken.None).ConfigureAwait(false);
 241            _logger.LogDebug("Opening XmlTvReader for {Path}", path);
 242            var reader = new XmlTvReader(path, GetLanguage(info));
 243            IEnumerable<XmlTvChannel> results = reader.GetChannels();
 244
 245            // Should this method be async?
 246            return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList();
 247        }
 248
 249        public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
 250        {
 251            // In theory this should never be called because there is always only one lineup
 252            string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
 253            _logger.LogDebug("Opening XmlTvReader for {Path}", path);
 254            var reader = new XmlTvReader(path, GetLanguage(info));
 255            var results = reader.GetChannels();
 256
 257            // Should this method be async?
 258            return results.Select(c => new ChannelInfo
 259            {
 260                Id = c.Id,
 261                Name = c.DisplayName,
 262                ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
 263                Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
 264            }).ToList();
 265        }
 266    }
 267}