< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Plugins.Tmdb.TV.TmdbUpcomingEpisodesTask
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbUpcomingEpisodesTask.cs
Line coverage
15%
Covered lines: 13
Uncovered lines: 72
Coverable lines: 85
Total lines: 207
Line coverage: 15.2%
Branch coverage
6%
Covered branches: 2
Total branches: 30
Branch coverage: 6.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/3/2026 - 12:16:46 AM Line coverage: 15.2% (13/85) Branch coverage: 6.6% (2/30) Total lines: 207 8/3/2026 - 12:16:46 AM Line coverage: 15.2% (13/85) Branch coverage: 6.6% (2/30) Total lines: 207

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%11100%
get_Description()100%210%
get_Category()100%210%
get_Key()100%210%
GetDefaultTriggers()50%4488.88%
ExecuteAsync()0%210140%
IsEnabledForLibrary(...)0%4260%
RemoveAllVirtualItems(...)0%4260%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbUpcomingEpisodesTask.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Jellyfin.Data.Enums;
 8using MediaBrowser.Controller.Entities;
 9using MediaBrowser.Controller.Entities.TV;
 10using MediaBrowser.Controller.Library;
 11using MediaBrowser.Controller.Providers;
 12using MediaBrowser.Model.Entities;
 13using MediaBrowser.Model.IO;
 14using MediaBrowser.Model.Tasks;
 15using Microsoft.Extensions.Logging;
 16
 17namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 18{
 19    /// <summary>
 20    /// Scheduled task that re-checks TMDb for newly announced unaired and missing episodes and creates
 21    /// the corresponding virtual items. This keeps the "Upcoming" view current for series whose local
 22    /// files have not changed, which an ordinary library scan would never re-examine.
 23    /// </summary>
 24    public class TmdbUpcomingEpisodesTask : IScheduledTask
 25    {
 26        private const int DefaultIntervalDays = 7;
 27
 28        private readonly ILibraryManager _libraryManager;
 29        private readonly IFileSystem _fileSystem;
 30        private readonly ILogger<TmdbUpcomingEpisodesTask> _logger;
 31
 32        /// <summary>
 33        /// Initializes a new instance of the <see cref="TmdbUpcomingEpisodesTask"/> class.
 34        /// </summary>
 35        /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
 36        /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
 37        /// <param name="logger">The <see cref="ILogger{TmdbUpcomingEpisodesTask}"/>.</param>
 38        public TmdbUpcomingEpisodesTask(
 39            ILibraryManager libraryManager,
 40            IFileSystem fileSystem,
 41            ILogger<TmdbUpcomingEpisodesTask> logger)
 42        {
 2243            _libraryManager = libraryManager;
 2244            _fileSystem = fileSystem;
 2245            _logger = logger;
 2246        }
 47
 48        /// <inheritdoc />
 2249        public string Name => "Refresh upcoming and missing episodes (TheMovieDb)";
 50
 51        /// <inheritdoc />
 052        public string Description => "Checks TheMovieDb for newly announced episodes and creates virtual entries for una
 53
 54        /// <inheritdoc />
 055        public string Category => "Library";
 56
 57        /// <inheritdoc />
 058        public string Key => "TmdbRefreshUpcomingEpisodes";
 59
 60        /// <inheritdoc />
 61        public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
 62        {
 2263            var intervalDays = Plugin.Instance?.Configuration.MissingEpisodeRefreshIntervalDays ?? DefaultIntervalDays;
 2264            if (intervalDays <= 0)
 65            {
 066                intervalDays = DefaultIntervalDays;
 67            }
 68
 2269            yield return new TaskTriggerInfo
 2270            {
 2271                Type = TaskTriggerInfoType.IntervalTrigger,
 2272                IntervalTicks = TimeSpan.FromDays(intervalDays).Ticks
 2273            };
 2274        }
 75
 76        /// <inheritdoc />
 77        public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
 78        {
 079            var configuration = Plugin.Instance?.Configuration;
 080            if (configuration is null)
 81            {
 082                progress.Report(100);
 083                return;
 84            }
 85
 86            // The feature is fully disabled: remove every virtual episode (and now-empty virtual season)
 87            // this provider previously created, across all libraries, then stop.
 088            if ((!configuration.ImportUnairedEpisodes && !configuration.ImportMissingEpisodes)
 089                || configuration.EnabledMissingEpisodeLibraries.Length == 0)
 90            {
 091                RemoveAllVirtualItems(progress, cancellationToken);
 092                return;
 93            }
 94
 95            // Process non-ended series (they may have gained episodes) plus any series in a library that
 96            // is not opted in (regardless of status) so the provider can prune the virtual episodes it
 97            // previously created there. Ended series in enabled libraries cannot change, so they're skipped.
 098            var series = _libraryManager.GetItemList(new InternalItemsQuery
 099            {
 0100                IncludeItemTypes = [BaseItemKind.Series],
 0101                Recursive = true
 0102            })
 0103                .OfType<Series>()
 0104                .Where(s => s.HasProviderId(MetadataProvider.Tmdb)
 0105                    && (s.Status != SeriesStatus.Ended || !IsEnabledForLibrary(s)))
 0106                .ToList();
 107
 0108            if (series.Count == 0)
 109            {
 0110                progress.Report(100);
 0111                return;
 112            }
 113
 114            // ValidateChildren (rather than a bare RefreshMetadata) is required so the created episodes
 115            // are immediately visible.
 0116            var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0117            {
 0118                MetadataRefreshMode = MetadataRefreshMode.Default,
 0119                ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
 0120                IsAutomated = true
 0121            };
 122
 0123            for (var i = 0; i < series.Count; i++)
 124            {
 0125                cancellationToken.ThrowIfCancellationRequested();
 126
 127                try
 128                {
 0129                    await series[i].ValidateChildren(new Progress<double>(), refreshOptions, cancellationToken: cancella
 0130                }
 0131                catch (OperationCanceledException)
 132                {
 0133                    throw;
 134                }
 0135                catch (Exception ex)
 136                {
 0137                    _logger.LogError(ex, "Error refreshing upcoming episodes for series {SeriesName}", series[i].Name);
 0138                }
 139
 0140                progress.Report(100.0 * (i + 1) / series.Count);
 141            }
 0142        }
 143
 144        private bool IsEnabledForLibrary(BaseItem item)
 145        {
 0146            var enabledLibraries = Plugin.Instance?.Configuration.EnabledMissingEpisodeLibraries;
 0147            if (enabledLibraries is null || enabledLibraries.Length == 0)
 148            {
 0149                return false;
 150            }
 151
 152            // A series can live under more than one collection folder; opting in any one of them is
 153            // enough. An item that belongs to no collection folder cannot be opted in at all.
 0154            return _libraryManager.GetCollectionFolders(item).Any(folder =>
 0155                enabledLibraries.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalI
 156        }
 157
 158        /// <summary>
 159        /// Removes every virtual episode this provider created (identified by being virtual and carrying
 160        /// a TMDb id), plus any virtual season left without episodes as a result. Used when both import
 161        /// options are disabled so turning the feature off cleans up its placeholders.
 162        /// </summary>
 163        private void RemoveAllVirtualItems(IProgress<double> progress, CancellationToken cancellationToken)
 164        {
 0165            var deleteOptions = new DeleteOptions { DeleteFileLocation = false };
 166
 0167            var virtualEpisodes = _libraryManager.GetItemList(new InternalItemsQuery
 0168            {
 0169                IncludeItemTypes = [BaseItemKind.Episode],
 0170                IsVirtualItem = true,
 0171                HasTmdbId = true,
 0172                Recursive = true
 0173            });
 174
 0175            for (var i = 0; i < virtualEpisodes.Count; i++)
 176            {
 0177                cancellationToken.ThrowIfCancellationRequested();
 178
 0179                _logger.LogInformation("Removing virtual episode {Name}: the TMDb missing episode provider is disabled",
 0180                _libraryManager.DeleteItem(virtualEpisodes[i], deleteOptions, false);
 181
 0182                progress.Report(95.0 * (i + 1) / virtualEpisodes.Count);
 183            }
 184
 185            // Remove virtual seasons that are now empty (mirrors the cleanup an ordinary series refresh does).
 0186            var virtualSeasons = _libraryManager.GetItemList(new InternalItemsQuery
 0187            {
 0188                IncludeItemTypes = [BaseItemKind.Season],
 0189                IsVirtualItem = true,
 0190                HasTmdbId = true,
 0191                Recursive = true
 0192            });
 193
 0194            foreach (var season in virtualSeasons.OfType<Season>())
 195            {
 0196                cancellationToken.ThrowIfCancellationRequested();
 197
 0198                if (season.GetEpisodes().Count == 0)
 199                {
 0200                    _libraryManager.DeleteItem(season, deleteOptions, false);
 201                }
 202            }
 203
 0204            progress.Report(100);
 0205        }
 206    }
 207}