< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Plugins.Tmdb.TV.TmdbSeriesProvider
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
Line coverage
5%
Covered lines: 4
Uncovered lines: 70
Coverable lines: 74
Total lines: 413
Line coverage: 5.4%
Branch coverage
0%
Covered branches: 0
Total branches: 46
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/7/2025 - 12:11:55 AM Line coverage: 5.6% (4/71) Branch coverage: 0% (0/40) Total lines: 4121/19/2026 - 12:13:54 AM Line coverage: 5.6% (4/71) Branch coverage: 0% (0/40) Total lines: 4102/15/2026 - 12:13:43 AM Line coverage: 5.4% (4/74) Branch coverage: 0% (0/46) Total lines: 413 11/7/2025 - 12:11:55 AM Line coverage: 5.6% (4/71) Branch coverage: 0% (0/40) Total lines: 4121/19/2026 - 12:13:54 AM Line coverage: 5.6% (4/71) Branch coverage: 0% (0/40) Total lines: 4102/15/2026 - 12:13:43 AM Line coverage: 5.4% (4/74) Branch coverage: 0% (0/46) Total lines: 413

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%210%
get_Order()100%210%
MapTvShowToRemoteSearchResult(...)0%7280%
MapSearchTvToRemoteSearchResult(...)0%4260%
MapTvShowToSeries(...)0%1056320%
GetImageResponse(...)100%210%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using System.Net.Http;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Data.Enums;
 9using Jellyfin.Extensions;
 10using MediaBrowser.Common.Net;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Entities.TV;
 13using MediaBrowser.Controller.Library;
 14using MediaBrowser.Controller.Providers;
 15using MediaBrowser.Model.Entities;
 16using MediaBrowser.Model.Providers;
 17using TMDbLib.Objects.Find;
 18using TMDbLib.Objects.Search;
 19using TMDbLib.Objects.TvShows;
 20
 21namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 22{
 23    /// <summary>
 24    /// TV series provider powered by TheMovieDb.
 25    /// </summary>
 26    public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
 27    {
 28        private readonly IHttpClientFactory _httpClientFactory;
 29        private readonly ILibraryManager _libraryManager;
 30        private readonly TmdbClientManager _tmdbClientManager;
 31
 32        /// <summary>
 33        /// Initializes a new instance of the <see cref="TmdbSeriesProvider"/> class.
 34        /// </summary>
 35        /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
 36        /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
 37        /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param>
 38        public TmdbSeriesProvider(
 39            ILibraryManager libraryManager,
 40            IHttpClientFactory httpClientFactory,
 41            TmdbClientManager tmdbClientManager)
 42        {
 2143            _libraryManager = libraryManager;
 2144            _httpClientFactory = httpClientFactory;
 2145            _tmdbClientManager = tmdbClientManager;
 2146        }
 47
 48        /// <inheritdoc />
 049        public string Name => TmdbUtils.ProviderName;
 50
 51        /// <inheritdoc />
 052        public int Order => 1;
 53
 54        /// <inheritdoc />
 55        public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken can
 56        {
 57            if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId))
 58            {
 59                var series = await _tmdbClientManager
 60                    .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, 
 61                    .ConfigureAwait(false);
 62
 63                if (series is not null)
 64                {
 65                    var remoteResult = MapTvShowToRemoteSearchResult(series);
 66
 67                    return new[] { remoteResult };
 68                }
 69            }
 70
 71            if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
 72            {
 73                var findResult = await _tmdbClientManager
 74                    .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, searchInfo.Meta
 75                    .ConfigureAwait(false);
 76
 77                var tvResults = findResult?.TvResults;
 78                if (tvResults is not null)
 79                {
 80                    var imdbIdResults = new RemoteSearchResult[tvResults.Count];
 81                    for (var i = 0; i < tvResults.Count; i++)
 82                    {
 83                        var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]);
 84                        remoteResult.SetProviderId(MetadataProvider.Imdb, imdbId);
 85                        imdbIdResults[i] = remoteResult;
 86                    }
 87
 88                    return imdbIdResults;
 89                }
 90            }
 91
 92            if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
 93            {
 94                var findResult = await _tmdbClientManager
 95                    .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, searchInfo.Meta
 96                    .ConfigureAwait(false);
 97
 98                var tvResults = findResult?.TvResults;
 99                if (tvResults is not null)
 100                {
 101                    var tvIdResults = new RemoteSearchResult[tvResults.Count];
 102                    for (var i = 0; i < tvResults.Count; i++)
 103                    {
 104                        var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]);
 105                        remoteResult.SetProviderId(MetadataProvider.Tvdb, tvdbId);
 106                        tvIdResults[i] = remoteResult;
 107                    }
 108
 109                    return tvIdResults;
 110                }
 111            }
 112
 113            var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguag
 114                .ConfigureAwait(false);
 115
 116            var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
 117            for (var i = 0; i < tvSearchResults.Count; i++)
 118            {
 119                remoteResults[i] = MapSearchTvToRemoteSearchResult(tvSearchResults[i]);
 120            }
 121
 122            return remoteResults;
 123        }
 124
 125        private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series)
 126        {
 0127            var remoteResult = new RemoteSearchResult
 0128            {
 0129                Name = series.Name ?? series.OriginalName,
 0130                SearchProviderName = Name,
 0131                ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
 0132                Overview = series.Overview
 0133            };
 134
 0135            remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
 0136            if (series.ExternalIds is not null)
 137            {
 0138                remoteResult.TrySetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
 139
 0140                remoteResult.TrySetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
 141            }
 142
 0143            remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
 0144            remoteResult.ProductionYear = series.FirstAirDate?.Year;
 145
 0146            return remoteResult;
 147        }
 148
 149        private RemoteSearchResult MapSearchTvToRemoteSearchResult(SearchTv series)
 150        {
 0151            var remoteResult = new RemoteSearchResult
 0152            {
 0153                Name = series.Name ?? series.OriginalName,
 0154                SearchProviderName = Name,
 0155                ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
 0156                Overview = series.Overview
 0157            };
 158
 0159            remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
 0160            remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
 0161            remoteResult.ProductionYear = series.FirstAirDate?.Year;
 162
 0163            return remoteResult;
 164        }
 165
 166        /// <inheritdoc />
 167        public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
 168        {
 169            var result = new MetadataResult<Series>
 170            {
 171                QueriedById = true
 172            };
 173
 174            var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
 175
 176            if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
 177            {
 178                var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.
 179                if (searchResult?.TvResults.Count > 0)
 180                {
 181                    tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
 182                }
 183            }
 184
 185            if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
 186            {
 187                var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.
 188                if (searchResult?.TvResults.Count > 0)
 189                {
 190                    tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
 191                }
 192            }
 193
 194            if (string.IsNullOrEmpty(tmdbId))
 195            {
 196                result.QueriedById = false;
 197                // ParseName is required here.
 198                // Caller provides the filename with extension stripped and NOT the parsed filename
 199                var parsedName = _libraryManager.ParseName(info.Name);
 200                var cleanedName = TmdbUtils.CleanName(parsedName.Name);
 201                var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.
 202
 203                if (searchResults.Count > 0)
 204                {
 205                    tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
 206                }
 207            }
 208
 209            if (!int.TryParse(tmdbId, CultureInfo.InvariantCulture, out int tmdbIdInt))
 210            {
 211                return result;
 212            }
 213
 214            cancellationToken.ThrowIfCancellationRequested();
 215
 216            var tvShow = await _tmdbClientManager
 217                .GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage
 218                .ConfigureAwait(false);
 219
 220            if (tvShow is null)
 221            {
 222                return result;
 223            }
 224
 225            result = new MetadataResult<Series>
 226            {
 227                Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
 228                ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
 229            };
 230
 231            foreach (var person in GetPersons(tvShow))
 232            {
 233                result.AddPerson(person);
 234            }
 235
 236            result.HasMetadata = result.Item is not null;
 237
 238            return result;
 239        }
 240
 241        private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
 242        {
 0243            var series = new Series
 0244            {
 0245                Name = seriesResult.Name,
 0246                OriginalTitle = seriesResult.OriginalName
 0247            };
 248
 0249            series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture));
 250
 0251            series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage);
 252
 0253            series.Overview = seriesResult.Overview;
 254
 0255            if (seriesResult.Networks is not null)
 256            {
 0257                series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray();
 258            }
 259
 0260            if (seriesResult.Genres is not null)
 261            {
 0262                series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray();
 263            }
 264
 0265            if (seriesResult.Keywords?.Results is not null)
 266            {
 0267                for (var i = 0; i < seriesResult.Keywords.Results.Count; i++)
 268                {
 0269                    series.AddTag(seriesResult.Keywords.Results[i].Name);
 270                }
 271            }
 272
 0273            series.HomePageUrl = seriesResult.Homepage;
 274
 0275            series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(
 276
 0277            if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus))
 278            {
 0279                series.Status = seriesStatus;
 280            }
 281
 0282            series.EndDate = seriesResult.LastAirDate;
 0283            series.PremiereDate = seriesResult.FirstAirDate;
 0284            series.ProductionYear = seriesResult.FirstAirDate?.Year;
 285
 0286            var ids = seriesResult.ExternalIds;
 0287            if (ids is not null)
 288            {
 0289                series.TrySetProviderId(MetadataProvider.Imdb, ids.ImdbId);
 0290                series.TrySetProviderId(MetadataProvider.TvRage, ids.TvrageId);
 0291                series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
 292            }
 293
 0294            var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
 295
 0296            var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, String
 0297            var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.Ordina
 0298            var minimumRelease = contentRatings.FirstOrDefault();
 299
 0300            if (ourRelease is not null)
 301            {
 0302                series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating);
 303            }
 0304            else if (usRelease is not null)
 305            {
 0306                series.OfficialRating = usRelease.Rating;
 307            }
 0308            else if (minimumRelease is not null)
 309            {
 0310                series.OfficialRating = minimumRelease.Rating;
 311            }
 312
 0313            if (seriesResult.Videos?.Results is not null)
 314            {
 0315                foreach (var video in seriesResult.Videos.Results)
 316                {
 0317                    if (TmdbUtils.IsTrailerType(video))
 318                    {
 0319                        series.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key);
 320                    }
 321                }
 322            }
 323
 0324            return series;
 325        }
 326
 327        private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
 328        {
 329            var config = Plugin.Instance.Configuration;
 330
 331            if (seriesResult.Credits?.Cast is not null)
 332            {
 333                IEnumerable<Cast> castQuery = seriesResult.Credits.Cast.OrderBy(a => a.Order);
 334
 335                if (config.HideMissingCastMembers)
 336                {
 337                    castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath));
 338                }
 339
 340                foreach (var actor in castQuery.Take(config.MaxCastMembers))
 341                {
 342                    if (string.IsNullOrWhiteSpace(actor.Name))
 343                    {
 344                        continue;
 345                    }
 346
 347                    var personInfo = new PersonInfo
 348                    {
 349                        Name = actor.Name.Trim(),
 350                        Role = actor.Character?.Trim() ?? string.Empty,
 351                        Type = PersonKind.Actor,
 352                        SortOrder = actor.Order,
 353                        ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
 354                    };
 355
 356                    if (actor.Id > 0)
 357                    {
 358                        personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture))
 359                    }
 360
 361                    yield return personInfo;
 362                }
 363            }
 364
 365            if (seriesResult.Credits?.Crew is not null)
 366            {
 367                var crewQuery = seriesResult.Credits.Crew
 368                    .Select(crewMember => new
 369                    {
 370                        CrewMember = crewMember,
 371                        PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
 372                    })
 373                    .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
 374
 375                if (config.HideMissingCrewMembers)
 376                {
 377                    crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
 378                }
 379
 380                foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
 381                {
 382                    var crewMember = entry.CrewMember;
 383
 384                    if (string.IsNullOrWhiteSpace(crewMember.Name))
 385                    {
 386                        continue;
 387                    }
 388
 389                    var personInfo = new PersonInfo
 390                    {
 391                        Name = crewMember.Name.Trim(),
 392                        Role = crewMember.Job?.Trim() ?? string.Empty,
 393                        Type = entry.PersonType,
 394                        ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
 395                    };
 396
 397                    if (crewMember.Id > 0)
 398                    {
 399                        personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCult
 400                    }
 401
 402                    yield return personInfo;
 403                }
 404            }
 405        }
 406
 407        /// <inheritdoc />
 408        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
 409        {
 0410            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
 411        }
 412    }
 413}