< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Listings.XmlTvProgramEtag
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Listings/XmlTvProgramEtag.cs
Line coverage
79%
Covered lines: 77
Uncovered lines: 20
Coverable lines: 97
Total lines: 184
Line coverage: 79.3%
Branch coverage
57%
Covered branches: 23
Total branches: 40
Branch coverage: 57.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 7/22/2026 - 12:16:22 AM Line coverage: 79.3% (77/97) Branch coverage: 57.5% (23/40) Total lines: 184 7/22/2026 - 12:16:22 AM Line coverage: 79.3% (77/97) Branch coverage: 57.5% (23/40) Total lines: 184

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IsXmlTvEtag(...)50%22100%
MatchesStored(...)100%22100%
TryCreate(...)50%131281.81%
AppendValue(...)100%22100%
AppendValue(...)100%11100%
AppendValue(...)50%22100%
AppendValue(...)100%22100%
AppendValue(...)25%44100%
AppendValue(...)100%22100%
AppendValue(...)50%22100%
FormatDateTime(...)50%4471.42%
AppendList(...)100%22100%
AppendDictionary(...)25%10427.27%

File(s)

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

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.Linq;
 7using System.Security.Cryptography;
 8using System.Text;
 9using MediaBrowser.Controller.LiveTv;
 10
 11namespace Jellyfin.LiveTv.Listings
 12{
 13    internal static class XmlTvProgramEtag
 14    {
 15        internal const string Prefix = "xmltv-sha256-v1:";
 16
 17        internal static bool IsXmlTvEtag(string? etag)
 318            => !string.IsNullOrWhiteSpace(etag)
 319                && etag.StartsWith(Prefix, StringComparison.Ordinal);
 20
 21        // Returns true only when the incoming etag is XMLTV-style AND equals the stored value.
 22        // The IsXmlTvEtag gate keeps other providers (e.g. Schedules Direct) on the
 23        // field-by-field update path even if their etag strings happen to match.
 24        internal static bool MatchesStored(string? incomingEtag, string? storedEtag)
 325            => IsXmlTvEtag(incomingEtag)
 326                && string.Equals(incomingEtag, storedEtag, StringComparison.OrdinalIgnoreCase);
 27
 28        internal static bool TryCreate(ProgramInfo programInfo, out string? etag, out string? reason)
 29        {
 2230            etag = null;
 31
 2232            if (string.IsNullOrWhiteSpace(programInfo.Id))
 33            {
 034                reason = "program id is empty";
 035                return false;
 36            }
 37
 2238            if (string.IsNullOrWhiteSpace(programInfo.ChannelId))
 39            {
 040                reason = "channel id is empty";
 041                return false;
 42            }
 43
 2244            if (programInfo.StartDate == default)
 45            {
 046                reason = "start date is empty";
 047                return false;
 48            }
 49
 2250            if (programInfo.EndDate == default)
 51            {
 052                reason = "end date is empty";
 053                return false;
 54            }
 55
 2256            if (programInfo.EndDate <= programInfo.StartDate)
 57            {
 058                reason = "end date is not after start date";
 059                return false;
 60            }
 61
 2262            var builder = new StringBuilder(1024);
 63
 64            // Keep this list aligned with the ProgramInfo fields consumed by GuideManager.
 2265            AppendValue(builder, "schema", "xmltv-programinfo-v1");
 2266            AppendValue(builder, nameof(programInfo.Id), programInfo.Id);
 2267            AppendValue(builder, nameof(programInfo.ChannelId), programInfo.ChannelId);
 2268            AppendValue(builder, nameof(programInfo.Name), programInfo.Name);
 2269            AppendValue(builder, nameof(programInfo.OfficialRating), programInfo.OfficialRating);
 2270            AppendValue(builder, nameof(programInfo.Overview), programInfo.Overview);
 2271            AppendValue(builder, nameof(programInfo.StartDate), programInfo.StartDate);
 2272            AppendValue(builder, nameof(programInfo.EndDate), programInfo.EndDate);
 2273            AppendList(builder, nameof(programInfo.Genres), programInfo.Genres);
 2274            AppendValue(builder, nameof(programInfo.OriginalAirDate), programInfo.OriginalAirDate);
 2275            AppendValue(builder, nameof(programInfo.IsHD), programInfo.IsHD);
 2276            AppendValue(builder, nameof(programInfo.Audio), programInfo.Audio?.ToString());
 2277            AppendValue(builder, nameof(programInfo.CommunityRating), programInfo.CommunityRating);
 2278            AppendValue(builder, nameof(programInfo.IsRepeat), programInfo.IsRepeat);
 2279            AppendValue(builder, nameof(programInfo.EpisodeTitle), programInfo.EpisodeTitle);
 2280            AppendValue(builder, nameof(programInfo.ImagePath), programInfo.ImagePath);
 2281            AppendValue(builder, nameof(programInfo.ImageUrl), programInfo.ImageUrl);
 2282            AppendValue(builder, nameof(programInfo.ThumbImageUrl), programInfo.ThumbImageUrl);
 2283            AppendValue(builder, nameof(programInfo.LogoImageUrl), programInfo.LogoImageUrl);
 2284            AppendValue(builder, nameof(programInfo.BackdropImageUrl), programInfo.BackdropImageUrl);
 2285            AppendValue(builder, nameof(programInfo.IsMovie), programInfo.IsMovie);
 2286            AppendValue(builder, nameof(programInfo.IsSports), programInfo.IsSports);
 2287            AppendValue(builder, nameof(programInfo.IsSeries), programInfo.IsSeries);
 2288            AppendValue(builder, nameof(programInfo.IsLive), programInfo.IsLive);
 2289            AppendValue(builder, nameof(programInfo.IsNews), programInfo.IsNews);
 2290            AppendValue(builder, nameof(programInfo.IsKids), programInfo.IsKids);
 2291            AppendValue(builder, nameof(programInfo.IsPremiere), programInfo.IsPremiere);
 2292            AppendValue(builder, nameof(programInfo.ProductionYear), programInfo.ProductionYear);
 2293            AppendValue(builder, nameof(programInfo.SeriesId), programInfo.SeriesId);
 2294            AppendValue(builder, nameof(programInfo.ShowId), programInfo.ShowId);
 2295            AppendValue(builder, nameof(programInfo.SeasonNumber), programInfo.SeasonNumber);
 2296            AppendValue(builder, nameof(programInfo.EpisodeNumber), programInfo.EpisodeNumber);
 2297            AppendDictionary(builder, nameof(programInfo.ProviderIds), programInfo.ProviderIds);
 2298            AppendDictionary(builder, nameof(programInfo.SeriesProviderIds), programInfo.SeriesProviderIds);
 99
 22100            var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
 22101            etag = Prefix + Convert.ToHexString(hash);
 22102            reason = null;
 22103            return true;
 104        }
 105
 106        private static void AppendValue(StringBuilder builder, string name, string? value)
 107        {
 772108            builder.Append(name).Append('|');
 772109            if (value is null)
 110            {
 244111                builder.Append('N').Append("|0|");
 112            }
 113            else
 114            {
 528115                builder.Append('S')
 528116                    .Append('|')
 528117                    .Append(value.Length.ToString(CultureInfo.InvariantCulture))
 528118                    .Append('|')
 528119                    .Append(value);
 120            }
 121
 772122            builder.Append('\n');
 772123        }
 124
 125        private static void AppendValue(StringBuilder builder, string name, DateTime value)
 44126            => AppendValue(builder, name, FormatDateTime(value));
 127
 128        private static void AppendValue(StringBuilder builder, string name, DateTime? value)
 22129            => AppendValue(builder, name, value.HasValue ? FormatDateTime(value.Value) : null);
 130
 131        private static void AppendValue(StringBuilder builder, string name, bool value)
 176132            => AppendValue(builder, name, value ? "true" : "false");
 133
 134        private static void AppendValue(StringBuilder builder, string name, bool? value)
 44135            => AppendValue(builder, name, value switch { true => "true", false => "false", null => null });
 136
 137        private static void AppendValue(StringBuilder builder, string name, int? value)
 66138            => AppendValue(builder, name, value?.ToString(CultureInfo.InvariantCulture));
 139
 140        private static void AppendValue(StringBuilder builder, string name, float? value)
 22141            => AppendValue(builder, name, value?.ToString("R", CultureInfo.InvariantCulture));
 142
 143        // Treat Unspecified as UTC so the etag does not vary with the server's local timezone.
 144        private static string FormatDateTime(DateTime value)
 145        {
 44146            var utc = value.Kind switch
 44147            {
 44148                DateTimeKind.Utc => value,
 0149                DateTimeKind.Unspecified => DateTime.SpecifyKind(value, DateTimeKind.Utc),
 0150                _ => value.ToUniversalTime(),
 44151            };
 152
 44153            return utc.ToString("O", CultureInfo.InvariantCulture);
 154        }
 155
 156        private static void AppendList(StringBuilder builder, string name, IReadOnlyList<string> values)
 157        {
 22158            AppendValue(builder, name + ".Count", values.Count.ToString(CultureInfo.InvariantCulture));
 92159            for (var i = 0; i < values.Count; i++)
 160            {
 24161                AppendValue(builder, $"{name}[{i}]", values[i]);
 162            }
 22163        }
 164
 165        private static void AppendDictionary(StringBuilder builder, string name, IReadOnlyDictionary<string, string?> va
 166        {
 44167            AppendValue(builder, name + ".Count", values.Count.ToString(CultureInfo.InvariantCulture));
 44168            if (values.Count == 0)
 169            {
 44170                return;
 171            }
 172
 0173            var index = 0;
 0174            foreach (var (key, value) in values
 0175                .OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase)
 0176                .ThenBy(i => i.Key, StringComparer.Ordinal))
 177            {
 0178                AppendValue(builder, $"{name}[{index}].Key", key);
 0179                AppendValue(builder, $"{name}[{index}].Value", value);
 0180                index++;
 181            }
 0182        }
 183    }
 184}