< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.TV.SeriesMetadataService
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/TV/SeriesMetadataService.cs
Line coverage
4%
Covered lines: 3
Uncovered lines: 69
Coverable lines: 72
Total lines: 272
Line coverage: 4.1%
Branch coverage
0%
Covered branches: 0
Total branches: 50
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
MergeData(...)0%210140%
RemoveObsoleteSeasons(...)0%272160%
RemoveObsoleteEpisodes(...)0%210140%
DeleteEpisode(...)100%210%
GetValidSeasonNameForSeries(...)0%4260%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/TV/SeriesMetadataService.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.Linq;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using MediaBrowser.Controller.Configuration;
 10using MediaBrowser.Controller.Dto;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Entities.TV;
 13using MediaBrowser.Controller.Library;
 14using MediaBrowser.Controller.Providers;
 15using MediaBrowser.Model.Entities;
 16using MediaBrowser.Model.Globalization;
 17using MediaBrowser.Model.IO;
 18using MediaBrowser.Providers.Manager;
 19using Microsoft.Extensions.Logging;
 20
 21namespace MediaBrowser.Providers.TV
 22{
 23    public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
 24    {
 25        private readonly ILocalizationManager _localizationManager;
 26
 27        public SeriesMetadataService(
 28            IServerConfigurationManager serverConfigurationManager,
 29            ILogger<SeriesMetadataService> logger,
 30            IProviderManager providerManager,
 31            IFileSystem fileSystem,
 32            ILibraryManager libraryManager,
 33            ILocalizationManager localizationManager)
 2234            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
 35        {
 2236            _localizationManager = localizationManager;
 2237        }
 38
 39        public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions,
 40        {
 41            if (item is Series series)
 42            {
 43                var seasons = series.GetRecursiveChildren(i => i is Season).ToList();
 44
 45                foreach (var season in seasons)
 46                {
 47                    var hasUpdate = refreshOptions != null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMeta
 48                    if (hasUpdate)
 49                    {
 50                        await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAw
 51                    }
 52                }
 53            }
 54
 55            return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false);
 56        }
 57
 58        /// <inheritdoc />
 59        protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, Cancellat
 60        {
 61            await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
 62
 63            RemoveObsoleteEpisodes(item);
 64            RemoveObsoleteSeasons(item);
 65            await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
 66        }
 67
 68        /// <inheritdoc />
 69        protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] 
 70        {
 071            base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 72
 073            var sourceItem = source.Item;
 074            var targetItem = target.Item;
 75
 076            if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
 77            {
 078                targetItem.AirTime = sourceItem.AirTime;
 79            }
 80
 081            if (replaceData || !targetItem.Status.HasValue)
 82            {
 083                targetItem.Status = sourceItem.Status;
 84            }
 85
 086            if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0)
 87            {
 088                targetItem.AirDays = sourceItem.AirDays;
 89            }
 090        }
 91
 92        private void RemoveObsoleteSeasons(Series series)
 93        {
 94            // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtua
 095            var physicalSeasonNumbers = new HashSet<int>();
 096            var virtualSeasons = new List<Season>();
 097            foreach (var existingSeason in series.Children.OfType<Season>())
 98            {
 099                if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
 100                {
 0101                    physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
 102                }
 0103                else if (existingSeason.LocationType == LocationType.Virtual)
 104                {
 0105                    virtualSeasons.Add(existingSeason);
 106                }
 107            }
 108
 0109            foreach (var virtualSeason in virtualSeasons)
 110            {
 0111                var seasonNumber = virtualSeason.IndexNumber;
 112                // If there's a physical season with the same number or no episodes in the season, delete it
 0113                if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
 0114                    || virtualSeason.GetEpisodes().Count == 0)
 115                {
 0116                    Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason
 117
 0118                    LibraryManager.DeleteItem(
 0119                        virtualSeason,
 0120                        new DeleteOptions
 0121                        {
 0122                            // Internal metadata paths are removed regardless of this.
 0123                            DeleteFileLocation = false
 0124                        },
 0125                        false);
 126                }
 127            }
 0128        }
 129
 130        private void RemoveObsoleteEpisodes(Series series)
 131        {
 0132            var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
 0133            var numberOfEpisodes = episodes.Count;
 134            // TODO: O(n^2), but can it be done faster without overcomplicating it?
 0135            for (var i = 0; i < numberOfEpisodes; i++)
 136            {
 0137                var currentEpisode = episodes[i];
 138                // The outer loop only examines virtual episodes
 0139                if (!currentEpisode.IsVirtualItem)
 140                {
 141                    continue;
 142                }
 143
 144                // Virtual episodes without an episode number are practically orphaned and should be deleted
 0145                if (!currentEpisode.IndexNumber.HasValue)
 146                {
 0147                    DeleteEpisode(currentEpisode);
 0148                    continue;
 149                }
 150
 0151                for (var j = i + 1; j < numberOfEpisodes; j++)
 152                {
 0153                    var comparisonEpisode = episodes[j];
 154                    // The inner loop is only for "physical" episodes
 0155                    if (comparisonEpisode.IsVirtualItem
 0156                        || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
 0157                        || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
 158                    {
 159                        continue;
 160                    }
 161
 0162                    DeleteEpisode(currentEpisode);
 0163                    break;
 164                }
 165            }
 0166        }
 167
 168        private void DeleteEpisode(Episode episode)
 169        {
 0170            Logger.LogInformation(
 0171                "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
 0172                episode.ParentIndexNumber,
 0173                episode.IndexNumber,
 0174                episode.SeriesName);
 175
 0176            LibraryManager.DeleteItem(
 0177                episode,
 0178                new DeleteOptions
 0179                {
 0180                    // Internal metadata paths are removed regardless of this.
 0181                    DeleteFileLocation = false
 0182                },
 0183                false);
 0184        }
 185
 186        /// <summary>
 187        /// Creates seasons for all episodes if they don't exist.
 188        /// If no season number can be determined, a dummy season will be created.
 189        /// </summary>
 190        /// <param name="series">The series.</param>
 191        /// <param name="cancellationToken">The cancellation token.</param>
 192        /// <returns>The async task.</returns>
 193        private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
 194        {
 195            var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
 196            var seasons = seriesChildren.OfType<Season>().ToList();
 197            var uniqueSeasonNumbers = seriesChildren
 198                .OfType<Episode>()
 199                .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
 200                .Distinct();
 201
 202            // Loop through the unique season numbers
 203            foreach (var seasonNumber in uniqueSeasonNumbers)
 204            {
 205                // Null season numbers will have a 'dummy' season created because seasons are always required.
 206                var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
 207                if (existingSeason is null)
 208                {
 209                    var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
 210                    await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
 211                }
 212                else if (existingSeason.IsVirtualItem)
 213                {
 214                    var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber &
 215                    if (episodeCount > 0)
 216                    {
 217                        existingSeason.IsVirtualItem = false;
 218                        await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Con
 219                    }
 220                }
 221            }
 222        }
 223
 224        /// <summary>
 225        /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
 226        /// </summary>
 227        /// <param name="series">The series.</param>
 228        /// <param name="seasonName">The season name.</param>
 229        /// <param name="seasonNumber">The season number.</param>
 230        /// <param name="cancellationToken">The cancellation token.</param>
 231        /// <returns>The newly created season.</returns>
 232        private async Task CreateSeasonAsync(
 233            Series series,
 234            string? seasonName,
 235            int? seasonNumber,
 236            CancellationToken cancellationToken)
 237        {
 238            Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
 239
 240            var season = new Season
 241            {
 242                Name = seasonName,
 243                IndexNumber = seasonNumber,
 244                Id = LibraryManager.GetNewItemId(
 245                    series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
 246                    typeof(Season)),
 247                IsVirtualItem = false,
 248                SeriesId = series.Id,
 249                SeriesName = series.Name,
 250                SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
 251            };
 252
 253            series.AddChild(season);
 254            await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken
 255        }
 256
 257        private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
 258        {
 0259            if (string.IsNullOrEmpty(seasonName))
 260            {
 0261                seasonName = seasonNumber switch
 0262                {
 0263                    null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
 0264                    0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
 0265                    _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeason
 0266                };
 267            }
 268
 0269            return seasonName;
 270        }
 271    }
 272}