< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.TV.TVSeriesManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/TV/TVSeriesManager.cs
Line coverage
3%
Covered lines: 4
Uncovered lines: 100
Coverable lines: 104
Total lines: 272
Line coverage: 3.8%
Branch coverage
0%
Covered branches: 0
Total branches: 78
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: 4.8% (4/82) Branch coverage: 0% (0/36) Total lines: 2805/4/2026 - 12:15:16 AM Line coverage: 3.8% (4/104) Branch coverage: 0% (0/78) Total lines: 272 1/23/2026 - 12:11:06 AM Line coverage: 4.8% (4/82) Branch coverage: 0% (0/36) Total lines: 2805/4/2026 - 12:15:16 AM Line coverage: 3.8% (4/104) Branch coverage: 0% (0/78) Total lines: 272

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetNextUp(...)0%110100%
GetNextUp(...)0%7280%
GetNextUpBatched(...)0%600240%
DetermineNextEpisode(...)0%812280%
DetermineNextEpisodeForRewatching(...)100%210%
GetUniqueSeriesKey(...)100%210%
GetResult(...)0%7280%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/TV/TVSeriesManager.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using Jellyfin.Data;
 7using Jellyfin.Data.Enums;
 8using Jellyfin.Database.Implementations.Entities;
 9using Jellyfin.Database.Implementations.Enums;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Controller.Configuration;
 12using MediaBrowser.Controller.Dto;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Library;
 15using MediaBrowser.Controller.TV;
 16using MediaBrowser.Model.Querying;
 17using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 18using Series = MediaBrowser.Controller.Entities.TV.Series;
 19
 20namespace Emby.Server.Implementations.TV
 21{
 22    public class TVSeriesManager : ITVSeriesManager
 23    {
 24        private readonly IUserDataManager _userDataManager;
 25        private readonly ILibraryManager _libraryManager;
 26        private readonly IServerConfigurationManager _configurationManager;
 27
 28        public TVSeriesManager(IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationMan
 29        {
 2130            _userDataManager = userDataManager;
 2131            _libraryManager = libraryManager;
 2132            _configurationManager = configurationManager;
 2133        }
 34
 35        public QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options)
 36        {
 037            var user = query.User;
 38
 039            string? presentationUniqueKey = null;
 040            if (!query.SeriesId.IsNullOrEmpty())
 41            {
 042                if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
 43                {
 044                    presentationUniqueKey = GetUniqueSeriesKey(series);
 45                }
 46            }
 47
 048            if (!string.IsNullOrEmpty(presentationUniqueKey))
 49            {
 050                return GetNextUpBatched(query, user, [presentationUniqueKey], options);
 51            }
 52
 53            BaseItem[] parents;
 54
 055            if (query.ParentId.HasValue)
 56            {
 057                var parent = _libraryManager.GetItemById(query.ParentId.Value);
 58
 059                if (parent is not null)
 60                {
 061                    parents = [parent];
 62                }
 63                else
 64                {
 065                    parents = [];
 66                }
 67            }
 68            else
 69            {
 070                parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
 071                   .Where(i => i is Folder)
 072                   .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes).Contains(i.Id))
 073                   .ToArray();
 74            }
 75
 076            return GetNextUp(query, parents, options);
 77        }
 78
 79        public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options)
 80        {
 081            var user = request.User;
 82
 083            string? presentationUniqueKey = null;
 084            int? limit = null;
 085            if (!request.SeriesId.IsNullOrEmpty())
 86            {
 087                if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
 88                {
 089                    presentationUniqueKey = GetUniqueSeriesKey(series);
 090                    limit = 1;
 91                }
 92            }
 93
 094            if (!string.IsNullOrEmpty(presentationUniqueKey))
 95            {
 096                return GetNextUpBatched(request, user, [presentationUniqueKey], options);
 97            }
 98
 099            if (limit.HasValue)
 100            {
 0101                limit = limit.Value + 10;
 102            }
 103
 0104            var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, p
 105
 0106            return GetNextUpBatched(request, user, nextUpSeriesKeys, options);
 107        }
 108
 109        private QueryResult<BaseItem> GetNextUpBatched(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys,
 110        {
 0111            if (seriesKeys.Count == 0)
 112            {
 0113                return new QueryResult<BaseItem>();
 114            }
 115
 0116            var includeSpecials = _configurationManager.Configuration.DisplaySpecialsWithinSeasons;
 0117            var includeRewatching = request.EnableRewatching;
 118
 0119            var query = new InternalItemsQuery(user)
 0120            {
 0121                DtoOptions = dtoOptions
 0122            };
 123
 0124            var batchResult = _libraryManager.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeRewatchi
 125
 0126            var nextUpList = new List<(DateTime LastWatchedDate, Episode Episode)>();
 127
 0128            foreach (var seriesKey in seriesKeys)
 129            {
 0130                if (!batchResult.TryGetValue(seriesKey, out var result))
 131                {
 132                    continue;
 133                }
 134
 0135                var nextEpisode = DetermineNextEpisode(result, user, includeSpecials, request.EnableResumable, false);
 136
 0137                if (nextEpisode is not null)
 138                {
 0139                    DateTime lastWatchedDate = DateTime.MinValue;
 0140                    if (result.LastWatched is not null)
 141                    {
 0142                        var userData = _userDataManager.GetUserData(user, result.LastWatched);
 0143                        lastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
 144                    }
 145
 0146                    nextUpList.Add((lastWatchedDate, nextEpisode));
 147                }
 148
 0149                if (includeRewatching)
 150                {
 0151                    var nextPlayedEpisode = DetermineNextEpisodeForRewatching(result, user, includeSpecials);
 152
 0153                    if (nextPlayedEpisode is not null)
 154                    {
 0155                        DateTime rewatchLastWatchedDate = DateTime.MinValue;
 0156                        if (result.LastWatchedForRewatching is not null)
 157                        {
 0158                            var userData = _userDataManager.GetUserData(user, result.LastWatchedForRewatching);
 0159                            rewatchLastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
 160                        }
 161
 0162                        nextUpList.Add((rewatchLastWatchedDate, nextPlayedEpisode));
 163                    }
 164                }
 165            }
 166
 0167            var sortedEpisodes = nextUpList
 0168                .OrderByDescending(x => x.LastWatchedDate)
 0169                .Select(x => (BaseItem)x.Episode);
 170
 0171            return GetResult(sortedEpisodes, request);
 172        }
 173
 174        private Episode? DetermineNextEpisode(
 175            MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
 176            User user,
 177            bool includeSpecials,
 178            bool includeResumable,
 179            bool includePlayed)
 180        {
 0181            var nextEpisode = (includePlayed ? result.NextPlayedForRewatching : result.NextUp) as Episode;
 0182            var lastWatchedEpisode = (includePlayed ? result.LastWatchedForRewatching : result.LastWatched) as Episode;
 183
 0184            if (includeSpecials && result.Specials?.Count > 0)
 185            {
 0186                var consideredEpisodes = result.Specials
 0187                    .Cast<Episode>()
 0188                    .Where(episode => episode.AirsBeforeSeasonNumber is not null || episode.AirsAfterSeasonNumber is not
 0189                    .ToList();
 190
 0191                if (lastWatchedEpisode is not null)
 192                {
 0193                    consideredEpisodes.Add(lastWatchedEpisode);
 194                }
 195
 0196                if (nextEpisode is not null)
 197                {
 0198                    consideredEpisodes.Add(nextEpisode);
 199                }
 200
 0201                if (consideredEpisodes.Count > 0)
 202                {
 0203                    var sortedEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, 
 0204                        .Cast<Episode>();
 205
 0206                    if (lastWatchedEpisode is not null)
 207                    {
 0208                        sortedEpisodes = sortedEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).
 209                    }
 210
 0211                    if (!includePlayed)
 212                    {
 0213                        sortedEpisodes = sortedEpisodes.Where(episode => _userDataManager.GetUserData(user, episode) is 
 214                    }
 215
 0216                    nextEpisode = sortedEpisodes.FirstOrDefault();
 217                }
 218            }
 219
 0220            if (nextEpisode is not null && !includeResumable)
 221            {
 0222                var userData = _userDataManager.GetUserData(user, nextEpisode);
 0223                if (userData?.PlaybackPositionTicks > 0)
 224                {
 0225                    return null;
 226                }
 227            }
 228
 0229            return nextEpisode;
 230        }
 231
 232        private Episode? DetermineNextEpisodeForRewatching(
 233            MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
 234            User user,
 235            bool includeSpecials)
 236        {
 0237            return DetermineNextEpisode(result, user, includeSpecials, includeResumable: false, includePlayed: true);
 238        }
 239
 240        private static string GetUniqueSeriesKey(Series series)
 241        {
 0242            return series.GetPresentationUniqueKey();
 243        }
 244
 245        private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
 246        {
 0247            int totalCount = 0;
 248
 0249            if (query.EnableTotalRecordCount)
 250            {
 0251                var list = items.ToList();
 0252                totalCount = list.Count;
 0253                items = list;
 254            }
 255
 0256            if (query.StartIndex.HasValue)
 257            {
 0258                items = items.Skip(query.StartIndex.Value);
 259            }
 260
 0261            if (query.Limit.HasValue && query.Limit.Value > 0)
 262            {
 0263                items = items.Take(query.Limit.Value);
 264            }
 265
 0266            return new QueryResult<BaseItem>(
 0267                query.StartIndex,
 0268                totalCount,
 0269                items.ToArray());
 270        }
 271    }
 272}