< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.TV.Series
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/TV/Series.cs
Line coverage
2%
Covered lines: 5
Uncovered lines: 231
Coverable lines: 236
Total lines: 550
Line coverage: 2.1%
Branch coverage
0%
Covered branches: 0
Total branches: 88
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/6/2026 - 12:15:23 AM Line coverage: 1.7% (4/227) Branch coverage: 0% (0/82) Total lines: 5266/2/2026 - 12:15:49 AM Line coverage: 3.5% (8/227) Branch coverage: 0% (0/82) Total lines: 5267/18/2026 - 12:15:19 AM Line coverage: 2.2% (5/227) Branch coverage: 0% (0/82) Total lines: 5267/27/2026 - 12:16:14 AM Line coverage: 2.1% (5/236) Branch coverage: 0% (0/88) Total lines: 550 5/6/2026 - 12:15:23 AM Line coverage: 1.7% (4/227) Branch coverage: 0% (0/82) Total lines: 5266/2/2026 - 12:15:49 AM Line coverage: 3.5% (8/227) Branch coverage: 0% (0/82) Total lines: 5267/18/2026 - 12:15:19 AM Line coverage: 2.2% (5/227) Branch coverage: 0% (0/82) Total lines: 5267/27/2026 - 12:16:14 AM Line coverage: 2.1% (5/236) Branch coverage: 0% (0/88) Total lines: 550

Coverage delta

Coverage delta 2 -2

Metrics

File(s)

/srv/git/jellyfin/MediaBrowser.Controller/Entities/TV/Series.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Linq;
 8using System.Text.Json.Serialization;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Jellyfin.Data;
 12using Jellyfin.Data.Enums;
 13using Jellyfin.Database.Implementations.Entities;
 14using Jellyfin.Database.Implementations.Enums;
 15using MediaBrowser.Controller.Dto;
 16using MediaBrowser.Controller.Providers;
 17using MediaBrowser.Model.Entities;
 18using MediaBrowser.Model.Querying;
 19using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
 20
 21namespace MediaBrowser.Controller.Entities.TV
 22{
 23    /// <summary>
 24    /// Class Series.
 25    /// </summary>
 26    public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer, ISuppo
 27    {
 16928        public Series()
 29        {
 16930            AirDays = Array.Empty<DayOfWeek>();
 16931        }
 32
 33        public DayOfWeek[] AirDays { get; set; }
 34
 35        public string AirTime { get; set; }
 36
 37        [JsonIgnore]
 038        public override bool SupportsAddingToPlaylist => true;
 39
 40        [JsonIgnore]
 041        public override bool IsPreSorted => true;
 42
 43        [JsonIgnore]
 044        public override bool SupportsDateLastMediaAdded => true;
 45
 46        [JsonIgnore]
 347        public override bool SupportsInheritedParentImages => false;
 48
 49        [JsonIgnore]
 050        public override bool SupportsPeople => true;
 51
 52        /// <inheritdoc />
 53        [JsonIgnore]
 054        public IReadOnlyList<BaseItem> LocalTrailers => GetExtras([Model.Entities.ExtraType.Trailer]).ToArray();
 55
 56        /// <summary>
 57        /// Gets or sets the display order.
 58        /// </summary>
 59        /// <remarks>
 60        /// Valid options are airdate, dvd or absolute.
 61        /// </remarks>
 62        public string DisplayOrder { get; set; }
 63
 64        /// <summary>
 65        /// Gets or sets the status.
 66        /// </summary>
 67        /// <value>The status.</value>
 68        public SeriesStatus? Status { get; set; }
 69
 70        public override double GetDefaultPrimaryImageAspectRatio()
 71        {
 072            double value = 2;
 073            value /= 3;
 74
 075            return value;
 76        }
 77
 78        public override string CreatePresentationUniqueKey()
 79        {
 080            if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
 81            {
 082                var userdatakeys = GetUserDataKeys();
 83
 84                // The first user data key is a stable cross-folder identity.
 85                // When none exists, fall back to the (normalized) series name.
 086                var groupingKey = userdatakeys.Count > 1
 087                    ? userdatakeys[0]
 088                    : GetNameBasedGroupingKey();
 89
 090                if (!string.IsNullOrEmpty(groupingKey))
 91                {
 092                    return AppendPreferredLanguage(groupingKey);
 93                }
 94            }
 95
 096            return base.CreatePresentationUniqueKey();
 97        }
 98
 99        // The owning libraries are deliberately NOT part of the key.
 100        private string AppendPreferredLanguage(string key)
 101        {
 0102            var lang = GetPreferredMetadataLanguage();
 0103            if (!string.IsNullOrEmpty(lang))
 104            {
 0105                key += "-" + lang;
 106            }
 107
 0108            return key;
 109        }
 110
 111        private string GetNameBasedGroupingKey()
 112        {
 113            // Prefix with the type so a series can never collide with a same-named item of another kind.
 0114            return string.IsNullOrEmpty(Name)
 0115                ? null
 0116                : "series-" + Name.ToLowerInvariant();
 117        }
 118
 119        private static string GetUniqueSeriesKey(BaseItem series)
 120        {
 0121            return series.GetPresentationUniqueKey();
 122        }
 123
 124        public override int GetChildCount(User user)
 125        {
 0126            var seriesKey = GetUniqueSeriesKey(this);
 127
 0128            var result = LibraryManager.GetCount(new InternalItemsQuery(user)
 0129            {
 0130                AncestorWithPresentationUniqueKey = null,
 0131                SeriesPresentationUniqueKey = seriesKey,
 0132                IncludeItemTypes = new[] { BaseItemKind.Season },
 0133                IsVirtualItem = false,
 0134                Limit = 0,
 0135                DtoOptions = new DtoOptions(false)
 0136                {
 0137                    EnableImages = false
 0138                }
 0139            });
 140
 0141            return result;
 142        }
 143
 144        public override int GetRecursiveChildCount(User user)
 145        {
 0146            var seriesKey = GetUniqueSeriesKey(this);
 147
 0148            var query = new InternalItemsQuery(user)
 0149            {
 0150                AncestorWithPresentationUniqueKey = null,
 0151                SeriesPresentationUniqueKey = seriesKey,
 0152                DtoOptions = new DtoOptions(false)
 0153                {
 0154                    EnableImages = false
 0155                }
 0156            };
 157
 0158            if (query.IncludeItemTypes.Length == 0)
 159            {
 0160                query.IncludeItemTypes = new[] { BaseItemKind.Episode };
 161            }
 162
 0163            query.IsVirtualItem = false;
 0164            query.Limit = 0;
 0165            var totalRecordCount = LibraryManager.GetCount(query);
 166
 0167            return totalRecordCount;
 168        }
 169
 170        /// <summary>
 171        /// Gets the user data key.
 172        /// </summary>
 173        /// <returns>System.String.</returns>
 174        public override List<string> GetUserDataKeys()
 175        {
 0176            var list = base.GetUserDataKeys();
 177
 0178            if (this.TryGetProviderId(MetadataProvider.Imdb, out var key))
 179            {
 0180                list.Insert(0, key);
 181            }
 182
 0183            if (this.TryGetProviderId(MetadataProvider.Tvdb, out key))
 184            {
 0185                list.Insert(0, key);
 186            }
 187
 0188            if (this.TryGetProviderId(MetadataProvider.Custom, out key))
 189            {
 0190                list.Insert(0, key);
 191            }
 192
 0193            return list;
 194        }
 195
 196        /// <inheritdoc />
 197        protected override Guid[] GetExtraOwnerIds()
 198        {
 0199            if (!LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
 200            {
 0201                return base.GetExtraOwnerIds();
 202            }
 203
 204            // Setting PresentationUniqueKey on the query disables presentation-key grouping, so this
 205            // returns every folder-item of the merged series rather than the collapsed survivor.
 0206            var ids = LibraryManager.GetItemIds(new InternalItemsQuery
 0207            {
 0208                PresentationUniqueKey = GetPresentationUniqueKey(),
 0209                IncludeItemTypes = [BaseItemKind.Series]
 0210            });
 211
 0212            return ids.Count == 0 ? base.GetExtraOwnerIds() : ids.ToArray();
 213        }
 214
 215        public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery qu
 216        {
 0217            return GetSeasons(user, new DtoOptions(true));
 218        }
 219
 220        public IReadOnlyList<BaseItem> GetSeasons(User user, DtoOptions options)
 221        {
 0222            var query = new InternalItemsQuery(user)
 0223            {
 0224                DtoOptions = options
 0225            };
 226
 0227            SetSeasonQueryOptions(query, user);
 228
 0229            return LibraryManager.GetItemList(query);
 230        }
 231
 232        private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
 233        {
 0234            var seriesKey = GetUniqueSeriesKey(this);
 235
 0236            query.AncestorWithPresentationUniqueKey = null;
 0237            query.SeriesPresentationUniqueKey = seriesKey;
 0238            query.IncludeItemTypes = new[] { BaseItemKind.Season };
 0239            query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
 240
 0241            if (user is not null && !user.DisplayMissingEpisodes)
 242            {
 0243                query.IsMissing = false;
 244            }
 0245        }
 246
 247        protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 248        {
 0249            var user = query.User;
 250
 0251            if (SourceType == SourceType.Channel)
 252            {
 253                try
 254                {
 0255                    query.Parent = this;
 0256                    query.ChannelIds = [ChannelId];
 0257                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 258                }
 0259                catch
 260                {
 261                    // Already logged at lower levels
 0262                    return new QueryResult<BaseItem>();
 263                }
 264            }
 265
 0266            if (query.Recursive)
 267            {
 0268                var seriesKey = GetUniqueSeriesKey(this);
 269
 0270                query.AncestorWithPresentationUniqueKey = null;
 0271                query.SeriesPresentationUniqueKey = seriesKey;
 0272                if (query.OrderBy.Count == 0)
 273                {
 0274                    query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
 275                }
 276
 0277                if (query.IncludeItemTypes.Length == 0)
 278                {
 0279                    query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season };
 280                }
 281
 0282                query.IsVirtualItem = false;
 0283                return LibraryManager.GetItemsResult(query);
 284            }
 285
 0286            SetSeasonQueryOptions(query, user);
 287
 0288            return LibraryManager.GetItemsResult(query);
 0289        }
 290
 291        public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
 292        {
 0293            var seriesKey = GetUniqueSeriesKey(this);
 294
 0295            var query = new InternalItemsQuery(user)
 0296            {
 0297                AncestorWithPresentationUniqueKey = null,
 0298                SeriesPresentationUniqueKey = seriesKey,
 0299                IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
 0300                OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
 0301                DtoOptions = options,
 0302            };
 303
 0304            if (!shouldIncludeMissingEpisodes)
 305            {
 0306                query.IsMissing = false;
 307            }
 308
 0309            var allItems = LibraryManager.GetItemList(query);
 310
 0311            var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
 312
 0313            var allEpisodes = allItems.OfType<Season>()
 0314                .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
 0315                .Reverse();
 316
 317            // Specials could appear twice based on above - once in season 0, once in the aired season
 318            // This depends on settings for that series
 319            // When this happens, remove the duplicate from season 0
 320
 0321            return allEpisodes.DistinctBy(i => i.Id).Reverse();
 322        }
 323
 324        public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, Cancella
 325        {
 0326            Children = null; // invalidate cached children.
 327            // Refresh bottom up, seasons and episodes first, then the series
 0328            var items = GetRecursiveChildren();
 329
 0330            var totalItems = items.Count;
 0331            var numComplete = 0;
 332
 333            // Refresh seasons
 0334            foreach (var item in items)
 335            {
 0336                if (item is not Season)
 337                {
 338                    continue;
 339                }
 340
 0341                cancellationToken.ThrowIfCancellationRequested();
 342
 0343                if (refreshOptions.RefreshItem(item))
 344                {
 0345                    await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 346                }
 347
 0348                numComplete++;
 0349                double percent = numComplete;
 0350                percent /= totalItems;
 0351                progress.Report(percent * 100);
 352            }
 353
 354            // Refresh episodes and other children
 0355            foreach (var item in items)
 356            {
 0357                if (item is Season)
 358                {
 359                    continue;
 360                }
 361
 0362                cancellationToken.ThrowIfCancellationRequested();
 363
 0364                bool skipItem = item is Episode episode
 0365                    && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
 0366                    && !refreshOptions.ReplaceAllMetadata
 0367                    && episode.IsMissingEpisode
 0368                    && episode.LocationType == LocationType.Virtual
 0369                    && episode.PremiereDate.HasValue
 0370                    && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30;
 371
 0372                if (!skipItem)
 373                {
 0374                    if (refreshOptions.RefreshItem(item))
 375                    {
 0376                        await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 377                    }
 378                }
 379
 0380                numComplete++;
 0381                double percent = numComplete;
 0382                percent /= totalItems;
 0383                progress.Report(percent * 100);
 384            }
 385
 0386            refreshOptions = new MetadataRefreshOptions(refreshOptions);
 0387            await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
 0388        }
 389
 390        public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMi
 391        {
 0392            var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
 393
 394            // add optimization when this setting is not enabled
 0395            var seriesKey = queryFromSeries ?
 0396                GetUniqueSeriesKey(this) :
 0397                GetUniqueSeriesKey(parentSeason);
 398
 0399            var query = new InternalItemsQuery(user)
 0400            {
 0401                AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
 0402                SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
 0403                IncludeItemTypes = new[] { BaseItemKind.Episode },
 0404                OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
 0405                DtoOptions = options
 0406            };
 407
 0408            if (!shouldIncludeMissingEpisodes)
 409            {
 0410                query.IsMissing = false;
 411            }
 412
 413            IReadOnlyList<BaseItem> allItems;
 0414            if (SourceType == SourceType.Channel)
 415            {
 416                try
 417                {
 0418                    query.Parent = parentSeason;
 0419                    query.ChannelIds = [ChannelId];
 0420                    allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationTok
 0421                }
 0422                catch
 423                {
 424                    // Already logged at lower levels
 0425                    return [];
 426                }
 427            }
 428            else
 429            {
 0430                allItems = LibraryManager.GetItemList(query);
 431            }
 432
 0433            return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
 0434        }
 435
 436        public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes,
 437        {
 0438            if (allSeriesEpisodes is null)
 439            {
 0440                return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
 441            }
 442
 0443            var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.Di
 444
 0445            var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
 446
 0447            return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList();
 448        }
 449
 450        /// <summary>
 451        /// Filters the episodes by season.
 452        /// </summary>
 453        /// <param name="episodes">The episodes.</param>
 454        /// <param name="parentSeason">The season.</param>
 455        /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
 456        /// <returns>The set of episodes.</returns>
 457        public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, 
 458        {
 0459            var seasonNumber = parentSeason.IndexNumber;
 0460            var seasonPresentationKey = GetUniqueSeriesKey(parentSeason);
 461
 0462            var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0;
 463
 0464            return episodes.Where(episode =>
 0465            {
 0466                var episodeItem = (Episode)episode;
 0467
 0468                var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexN
 0469                if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.V
 0470                {
 0471                    return true;
 0472                }
 0473
 0474                if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType
 0475                {
 0476                    return episodeItem.Season is null or { LocationType: LocationType.Virtual };
 0477                }
 0478
 0479                var season = episodeItem.Season;
 0480                return season is not null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComp
 0481            });
 482        }
 483
 484        /// <summary>
 485        /// Filters the episodes by season.
 486        /// </summary>
 487        /// <param name="episodes">The episodes.</param>
 488        /// <param name="seasonNumber">The season.</param>
 489        /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
 490        /// <returns>The set of episodes.</returns>
 491        public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool 
 492        {
 0493            if (!includeSpecials || seasonNumber < 1)
 494            {
 0495                return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
 496            }
 497
 0498            return episodes.Where(i =>
 0499            {
 0500                var episode = i;
 0501
 0502                if (episode is not null)
 0503                {
 0504                    var currentSeasonNumber = episode.AiredSeasonNumber;
 0505
 0506                    return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
 0507                }
 0508
 0509                return false;
 0510            });
 511        }
 512
 513        protected override bool GetBlockUnratedValue(User user)
 514        {
 0515            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
 516        }
 517
 518        public override UnratedItem GetBlockUnratedType()
 519        {
 10520            return UnratedItem.Series;
 521        }
 522
 523        public SeriesInfo GetLookupInfo()
 524        {
 0525            var info = GetItemLookupInfo<SeriesInfo>();
 526
 0527            return info;
 528        }
 529
 530        public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
 531        {
 0532            var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
 533
 0534            if (ProductionYear is null)
 535            {
 0536                var info = LibraryManager.ParseName(Name);
 537
 0538                var yearInName = info.Year;
 539
 0540                if (yearInName.HasValue)
 541                {
 0542                    ProductionYear = yearInName;
 0543                    hasChanges = true;
 544                }
 545            }
 546
 0547            return hasChanges;
 548        }
 549    }
 550}

Methods/Properties

.ctor()
get_SupportsAddingToPlaylist()
get_IsPreSorted()
get_SupportsDateLastMediaAdded()
get_SupportsInheritedParentImages()
get_SupportsPeople()
get_LocalTrailers()
GetDefaultPrimaryImageAspectRatio()
CreatePresentationUniqueKey()
AppendPreferredLanguage(System.String)
GetNameBasedGroupingKey()
GetUniqueSeriesKey(MediaBrowser.Controller.Entities.BaseItem)
GetChildCount(Jellyfin.Database.Implementations.Entities.User)
GetRecursiveChildCount(Jellyfin.Database.Implementations.Entities.User)
GetUserDataKeys()
GetExtraOwnerIds()
GetChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetSeasons(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions)
SetSeasonQueryOptions(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.Entities.User)
GetItemsInternal(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetEpisodes(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions,System.Boolean)
RefreshAllMetadata()
GetSeasonEpisodes(MediaBrowser.Controller.Entities.TV.Season,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions,System.Boolean)
GetSeasonEpisodes(MediaBrowser.Controller.Entities.TV.Season,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Dto.DtoOptions,System.Boolean)
FilterEpisodesBySeason(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.TV.Season,System.Boolean)
FilterEpisodesBySeason(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.TV.Episode>,System.Int32,System.Boolean)
GetBlockUnratedValue(Jellyfin.Database.Implementations.Entities.User)
GetBlockUnratedType()
GetLookupInfo()
BeforeMetadataRefresh(System.Boolean)