< 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
1%
Covered lines: 4
Uncovered lines: 223
Coverable lines: 227
Total lines: 526
Line coverage: 1.7%
Branch coverage
0%
Covered branches: 0
Total branches: 82
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: 2% (4/196) Branch coverage: 0% (0/56) Total lines: 5284/19/2026 - 12:14:27 AM Line coverage: 1.7% (4/229) Branch coverage: 0% (0/82) Total lines: 5285/4/2026 - 12:15:16 AM Line coverage: 1.7% (4/227) Branch coverage: 0% (0/82) Total lines: 526 1/23/2026 - 12:11:06 AM Line coverage: 2% (4/196) Branch coverage: 0% (0/56) Total lines: 5284/19/2026 - 12:14:27 AM Line coverage: 1.7% (4/229) Branch coverage: 0% (0/82) Total lines: 5285/4/2026 - 12:15:16 AM Line coverage: 1.7% (4/227) Branch coverage: 0% (0/82) Total lines: 526

Coverage delta

Coverage delta 1 -1

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.Globalization;
 8using System.Linq;
 9using System.Text.Json.Serialization;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Jellyfin.Data;
 13using Jellyfin.Data.Enums;
 14using Jellyfin.Database.Implementations.Entities;
 15using Jellyfin.Database.Implementations.Enums;
 16using MediaBrowser.Controller.Dto;
 17using MediaBrowser.Controller.Providers;
 18using MediaBrowser.Model.Entities;
 19using MediaBrowser.Model.Querying;
 20using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
 21
 22namespace MediaBrowser.Controller.Entities.TV
 23{
 24    /// <summary>
 25    /// Class Series.
 26    /// </summary>
 27    public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer, ISuppo
 28    {
 11529        public Series()
 30        {
 11531            AirDays = Array.Empty<DayOfWeek>();
 11532        }
 33
 34        public DayOfWeek[] AirDays { get; set; }
 35
 36        public string AirTime { get; set; }
 37
 38        [JsonIgnore]
 039        public override bool SupportsAddingToPlaylist => true;
 40
 41        [JsonIgnore]
 042        public override bool IsPreSorted => true;
 43
 44        [JsonIgnore]
 045        public override bool SupportsDateLastMediaAdded => true;
 46
 47        [JsonIgnore]
 048        public override bool SupportsInheritedParentImages => false;
 49
 50        [JsonIgnore]
 051        public override bool SupportsPeople => true;
 52
 53        /// <inheritdoc />
 54        [JsonIgnore]
 055        public IReadOnlyList<BaseItem> LocalTrailers => GetExtras([Model.Entities.ExtraType.Trailer]).ToArray();
 56
 57        /// <summary>
 58        /// Gets or sets the display order.
 59        /// </summary>
 60        /// <remarks>
 61        /// Valid options are airdate, dvd or absolute.
 62        /// </remarks>
 63        public string DisplayOrder { get; set; }
 64
 65        /// <summary>
 66        /// Gets or sets the status.
 67        /// </summary>
 68        /// <value>The status.</value>
 69        public SeriesStatus? Status { get; set; }
 70
 71        public override double GetDefaultPrimaryImageAspectRatio()
 72        {
 073            double value = 2;
 074            value /= 3;
 75
 076            return value;
 77        }
 78
 79        public override string CreatePresentationUniqueKey()
 80        {
 081            if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
 82            {
 083                var userdatakeys = GetUserDataKeys();
 84
 085                if (userdatakeys.Count > 1)
 86                {
 087                    return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
 88                }
 89            }
 90
 091            return base.CreatePresentationUniqueKey();
 92        }
 93
 94        private string AddLibrariesToPresentationUniqueKey(string key)
 95        {
 096            var lang = GetPreferredMetadataLanguage();
 097            if (!string.IsNullOrEmpty(lang))
 98            {
 099                key += "-" + lang;
 100            }
 101
 0102            var folders = LibraryManager.GetCollectionFolders(this)
 0103                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
 0104                .ToArray();
 105
 0106            if (folders.Length == 0)
 107            {
 0108                return key;
 109            }
 110
 0111            return key + "-" + string.Join('-', folders);
 112        }
 113
 114        private static string GetUniqueSeriesKey(BaseItem series)
 115        {
 0116            return series.GetPresentationUniqueKey();
 117        }
 118
 119        public override int GetChildCount(User user)
 120        {
 0121            var seriesKey = GetUniqueSeriesKey(this);
 122
 0123            var result = LibraryManager.GetCount(new InternalItemsQuery(user)
 0124            {
 0125                AncestorWithPresentationUniqueKey = null,
 0126                SeriesPresentationUniqueKey = seriesKey,
 0127                IncludeItemTypes = new[] { BaseItemKind.Season },
 0128                IsVirtualItem = false,
 0129                Limit = 0,
 0130                DtoOptions = new DtoOptions(false)
 0131                {
 0132                    EnableImages = false
 0133                }
 0134            });
 135
 0136            return result;
 137        }
 138
 139        public override int GetRecursiveChildCount(User user)
 140        {
 0141            var seriesKey = GetUniqueSeriesKey(this);
 142
 0143            var query = new InternalItemsQuery(user)
 0144            {
 0145                AncestorWithPresentationUniqueKey = null,
 0146                SeriesPresentationUniqueKey = seriesKey,
 0147                DtoOptions = new DtoOptions(false)
 0148                {
 0149                    EnableImages = false
 0150                }
 0151            };
 152
 0153            if (query.IncludeItemTypes.Length == 0)
 154            {
 0155                query.IncludeItemTypes = new[] { BaseItemKind.Episode };
 156            }
 157
 0158            query.IsVirtualItem = false;
 0159            query.Limit = 0;
 0160            var totalRecordCount = LibraryManager.GetCount(query);
 161
 0162            return totalRecordCount;
 163        }
 164
 165        /// <summary>
 166        /// Gets the user data key.
 167        /// </summary>
 168        /// <returns>System.String.</returns>
 169        public override List<string> GetUserDataKeys()
 170        {
 0171            var list = base.GetUserDataKeys();
 172
 0173            if (this.TryGetProviderId(MetadataProvider.Imdb, out var key))
 174            {
 0175                list.Insert(0, key);
 176            }
 177
 0178            if (this.TryGetProviderId(MetadataProvider.Tvdb, out key))
 179            {
 0180                list.Insert(0, key);
 181            }
 182
 0183            if (this.TryGetProviderId(MetadataProvider.Custom, out key))
 184            {
 0185                list.Insert(0, key);
 186            }
 187
 0188            return list;
 189        }
 190
 191        public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery qu
 192        {
 0193            return GetSeasons(user, new DtoOptions(true));
 194        }
 195
 196        public IReadOnlyList<BaseItem> GetSeasons(User user, DtoOptions options)
 197        {
 0198            var query = new InternalItemsQuery(user)
 0199            {
 0200                DtoOptions = options
 0201            };
 202
 0203            SetSeasonQueryOptions(query, user);
 204
 0205            return LibraryManager.GetItemList(query);
 206        }
 207
 208        private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
 209        {
 0210            var seriesKey = GetUniqueSeriesKey(this);
 211
 0212            query.AncestorWithPresentationUniqueKey = null;
 0213            query.SeriesPresentationUniqueKey = seriesKey;
 0214            query.IncludeItemTypes = new[] { BaseItemKind.Season };
 0215            query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
 216
 0217            if (user is not null && !user.DisplayMissingEpisodes)
 218            {
 0219                query.IsMissing = false;
 220            }
 0221        }
 222
 223        protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 224        {
 0225            var user = query.User;
 226
 0227            if (SourceType == SourceType.Channel)
 228            {
 229                try
 230                {
 0231                    query.Parent = this;
 0232                    query.ChannelIds = [ChannelId];
 0233                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 234                }
 0235                catch
 236                {
 237                    // Already logged at lower levels
 0238                    return new QueryResult<BaseItem>();
 239                }
 240            }
 241
 0242            if (query.Recursive)
 243            {
 0244                var seriesKey = GetUniqueSeriesKey(this);
 245
 0246                query.AncestorWithPresentationUniqueKey = null;
 0247                query.SeriesPresentationUniqueKey = seriesKey;
 0248                if (query.OrderBy.Count == 0)
 249                {
 0250                    query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
 251                }
 252
 0253                if (query.IncludeItemTypes.Length == 0)
 254                {
 0255                    query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season };
 256                }
 257
 0258                query.IsVirtualItem = false;
 0259                return LibraryManager.GetItemsResult(query);
 260            }
 261
 0262            SetSeasonQueryOptions(query, user);
 263
 0264            return LibraryManager.GetItemsResult(query);
 0265        }
 266
 267        public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
 268        {
 0269            var seriesKey = GetUniqueSeriesKey(this);
 270
 0271            var query = new InternalItemsQuery(user)
 0272            {
 0273                AncestorWithPresentationUniqueKey = null,
 0274                SeriesPresentationUniqueKey = seriesKey,
 0275                IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
 0276                OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
 0277                DtoOptions = options,
 0278            };
 279
 0280            if (!shouldIncludeMissingEpisodes)
 281            {
 0282                query.IsMissing = false;
 283            }
 284
 0285            var allItems = LibraryManager.GetItemList(query);
 286
 0287            var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
 288
 0289            var allEpisodes = allItems.OfType<Season>()
 0290                .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
 0291                .Reverse();
 292
 293            // Specials could appear twice based on above - once in season 0, once in the aired season
 294            // This depends on settings for that series
 295            // When this happens, remove the duplicate from season 0
 296
 0297            return allEpisodes.DistinctBy(i => i.Id).Reverse();
 298        }
 299
 300        public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, Cancella
 301        {
 0302            Children = null; // invalidate cached children.
 303            // Refresh bottom up, seasons and episodes first, then the series
 0304            var items = GetRecursiveChildren();
 305
 0306            var totalItems = items.Count;
 0307            var numComplete = 0;
 308
 309            // Refresh seasons
 0310            foreach (var item in items)
 311            {
 0312                if (item is not Season)
 313                {
 314                    continue;
 315                }
 316
 0317                cancellationToken.ThrowIfCancellationRequested();
 318
 0319                if (refreshOptions.RefreshItem(item))
 320                {
 0321                    await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 322                }
 323
 0324                numComplete++;
 0325                double percent = numComplete;
 0326                percent /= totalItems;
 0327                progress.Report(percent * 100);
 328            }
 329
 330            // Refresh episodes and other children
 0331            foreach (var item in items)
 332            {
 0333                if (item is Season)
 334                {
 335                    continue;
 336                }
 337
 0338                cancellationToken.ThrowIfCancellationRequested();
 339
 0340                bool skipItem = item is Episode episode
 0341                    && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
 0342                    && !refreshOptions.ReplaceAllMetadata
 0343                    && episode.IsMissingEpisode
 0344                    && episode.LocationType == LocationType.Virtual
 0345                    && episode.PremiereDate.HasValue
 0346                    && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30;
 347
 0348                if (!skipItem)
 349                {
 0350                    if (refreshOptions.RefreshItem(item))
 351                    {
 0352                        await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 353                    }
 354                }
 355
 0356                numComplete++;
 0357                double percent = numComplete;
 0358                percent /= totalItems;
 0359                progress.Report(percent * 100);
 360            }
 361
 0362            refreshOptions = new MetadataRefreshOptions(refreshOptions);
 0363            await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
 0364        }
 365
 366        public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMi
 367        {
 0368            var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
 369
 370            // add optimization when this setting is not enabled
 0371            var seriesKey = queryFromSeries ?
 0372                GetUniqueSeriesKey(this) :
 0373                GetUniqueSeriesKey(parentSeason);
 374
 0375            var query = new InternalItemsQuery(user)
 0376            {
 0377                AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
 0378                SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
 0379                IncludeItemTypes = new[] { BaseItemKind.Episode },
 0380                OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
 0381                DtoOptions = options
 0382            };
 383
 0384            if (!shouldIncludeMissingEpisodes)
 385            {
 0386                query.IsMissing = false;
 387            }
 388
 389            IReadOnlyList<BaseItem> allItems;
 0390            if (SourceType == SourceType.Channel)
 391            {
 392                try
 393                {
 0394                    query.Parent = parentSeason;
 0395                    query.ChannelIds = [ChannelId];
 0396                    allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationTok
 0397                }
 0398                catch
 399                {
 400                    // Already logged at lower levels
 0401                    return [];
 402                }
 403            }
 404            else
 405            {
 0406                allItems = LibraryManager.GetItemList(query);
 407            }
 408
 0409            return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
 0410        }
 411
 412        public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes,
 413        {
 0414            if (allSeriesEpisodes is null)
 415            {
 0416                return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
 417            }
 418
 0419            var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.Di
 420
 0421            var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
 422
 0423            return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList();
 424        }
 425
 426        /// <summary>
 427        /// Filters the episodes by season.
 428        /// </summary>
 429        /// <param name="episodes">The episodes.</param>
 430        /// <param name="parentSeason">The season.</param>
 431        /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
 432        /// <returns>The set of episodes.</returns>
 433        public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, 
 434        {
 0435            var seasonNumber = parentSeason.IndexNumber;
 0436            var seasonPresentationKey = GetUniqueSeriesKey(parentSeason);
 437
 0438            var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0;
 439
 0440            return episodes.Where(episode =>
 0441            {
 0442                var episodeItem = (Episode)episode;
 0443
 0444                var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexN
 0445                if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.V
 0446                {
 0447                    return true;
 0448                }
 0449
 0450                if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType
 0451                {
 0452                    return episodeItem.Season is null or { LocationType: LocationType.Virtual };
 0453                }
 0454
 0455                var season = episodeItem.Season;
 0456                return season is not null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComp
 0457            });
 458        }
 459
 460        /// <summary>
 461        /// Filters the episodes by season.
 462        /// </summary>
 463        /// <param name="episodes">The episodes.</param>
 464        /// <param name="seasonNumber">The season.</param>
 465        /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
 466        /// <returns>The set of episodes.</returns>
 467        public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool 
 468        {
 0469            if (!includeSpecials || seasonNumber < 1)
 470            {
 0471                return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
 472            }
 473
 0474            return episodes.Where(i =>
 0475            {
 0476                var episode = i;
 0477
 0478                if (episode is not null)
 0479                {
 0480                    var currentSeasonNumber = episode.AiredSeasonNumber;
 0481
 0482                    return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
 0483                }
 0484
 0485                return false;
 0486            });
 487        }
 488
 489        protected override bool GetBlockUnratedValue(User user)
 490        {
 0491            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
 492        }
 493
 494        public override UnratedItem GetBlockUnratedType()
 495        {
 10496            return UnratedItem.Series;
 497        }
 498
 499        public SeriesInfo GetLookupInfo()
 500        {
 0501            var info = GetItemLookupInfo<SeriesInfo>();
 502
 0503            return info;
 504        }
 505
 506        public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
 507        {
 0508            var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
 509
 0510            if (!ProductionYear.HasValue)
 511            {
 0512                var info = LibraryManager.ParseName(Name);
 513
 0514                var yearInName = info.Year;
 515
 0516                if (yearInName.HasValue)
 517                {
 0518                    ProductionYear = yearInName;
 0519                    hasChanges = true;
 520                }
 521            }
 522
 0523            return hasChanges;
 524        }
 525    }
 526}

Methods/Properties

.ctor()
get_SupportsAddingToPlaylist()
get_IsPreSorted()
get_SupportsDateLastMediaAdded()
get_SupportsInheritedParentImages()
get_SupportsPeople()
get_LocalTrailers()
GetDefaultPrimaryImageAspectRatio()
CreatePresentationUniqueKey()
AddLibrariesToPresentationUniqueKey(System.String)
GetUniqueSeriesKey(MediaBrowser.Controller.Entities.BaseItem)
GetChildCount(Jellyfin.Database.Implementations.Entities.User)
GetRecursiveChildCount(Jellyfin.Database.Implementations.Entities.User)
GetUserDataKeys()
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)