< 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: 72
Coverable lines: 76
Total lines: 423
Line coverage: 5.2%
Branch coverage
0%
Covered branches: 0
Total branches: 58
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/11/2025 - 12:09:58 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: 4132/19/2026 - 12:13:41 AM Line coverage: 5.2% (4/76) Branch coverage: 0% (0/58) Total lines: 423 11/11/2025 - 12:09:58 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: 4132/19/2026 - 12:13:41 AM Line coverage: 5.2% (4/76) Branch coverage: 0% (0/58) Total lines: 423

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%1980440%
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            if (tvSearchResults is null)
 116            {
 117                return [];
 118            }
 119
 120            var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
 121            for (var i = 0; i < tvSearchResults.Count; i++)
 122            {
 123                remoteResults[i] = MapSearchTvToRemoteSearchResult(tvSearchResults[i]);
 124            }
 125
 126            return remoteResults;
 127        }
 128
 129        private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series)
 130        {
 0131            var remoteResult = new RemoteSearchResult
 0132            {
 0133                Name = series.Name ?? series.OriginalName,
 0134                SearchProviderName = Name,
 0135                ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
 0136                Overview = series.Overview
 0137            };
 138
 0139            remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
 0140            if (series.ExternalIds is not null)
 141            {
 0142                remoteResult.TrySetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
 143
 0144                remoteResult.TrySetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
 145            }
 146
 0147            remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
 0148            remoteResult.ProductionYear = series.FirstAirDate?.Year;
 149
 0150            return remoteResult;
 151        }
 152
 153        private RemoteSearchResult MapSearchTvToRemoteSearchResult(SearchTv series)
 154        {
 0155            var remoteResult = new RemoteSearchResult
 0156            {
 0157                Name = series.Name ?? series.OriginalName,
 0158                SearchProviderName = Name,
 0159                ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
 0160                Overview = series.Overview
 0161            };
 162
 0163            remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
 0164            remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
 0165            remoteResult.ProductionYear = series.FirstAirDate?.Year;
 166
 0167            return remoteResult;
 168        }
 169
 170        /// <inheritdoc />
 171        public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
 172        {
 173            var result = new MetadataResult<Series>
 174            {
 175                QueriedById = true
 176            };
 177
 178            var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
 179
 180            if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
 181            {
 182                var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.
 183                if (searchResult?.TvResults?.Count > 0)
 184                {
 185                    tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
 186                }
 187            }
 188
 189            if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
 190            {
 191                var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.
 192                if (searchResult?.TvResults?.Count > 0)
 193                {
 194                    tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
 195                }
 196            }
 197
 198            if (string.IsNullOrEmpty(tmdbId))
 199            {
 200                result.QueriedById = false;
 201                // ParseName is required here.
 202                // Caller provides the filename with extension stripped and NOT the parsed filename
 203                var parsedName = _libraryManager.ParseName(info.Name);
 204                var cleanedName = TmdbUtils.CleanName(parsedName.Name);
 205                var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.
 206
 207                if (searchResults?.Count > 0)
 208                {
 209                    tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
 210                }
 211            }
 212
 213            if (!int.TryParse(tmdbId, CultureInfo.InvariantCulture, out int tmdbIdInt))
 214            {
 215                return result;
 216            }
 217
 218            cancellationToken.ThrowIfCancellationRequested();
 219
 220            var tvShow = await _tmdbClientManager
 221                .GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage
 222                .ConfigureAwait(false);
 223
 224            if (tvShow is null)
 225            {
 226                return result;
 227            }
 228
 229            result = new MetadataResult<Series>
 230            {
 231                Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
 232                ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
 233            };
 234
 235            foreach (var person in GetPersons(tvShow))
 236            {
 237                result.AddPerson(person);
 238            }
 239
 240            result.HasMetadata = result.Item is not null;
 241
 242            return result;
 243        }
 244
 245        private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
 246        {
 0247            var series = new Series
 0248            {
 0249                Name = seriesResult.Name,
 0250                OriginalTitle = seriesResult.OriginalName
 0251            };
 252
 0253            series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture));
 254
 0255            series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage);
 256
 0257            series.Overview = seriesResult.Overview;
 258
 0259            if (seriesResult.Networks is not null)
 260            {
 0261                series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray();
 262            }
 263
 0264            if (seriesResult.Genres is not null)
 265            {
 0266                series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray();
 267            }
 268
 0269            if (seriesResult.Keywords?.Results is not null)
 270            {
 0271                foreach (var result in seriesResult.Keywords.Results)
 272                {
 0273                    var name = result.Name;
 0274                    if (!string.IsNullOrWhiteSpace(name))
 275                    {
 0276                        series.AddTag(name);
 277                    }
 278                }
 279            }
 280
 0281            series.HomePageUrl = seriesResult.Homepage;
 282
 0283            series.RunTimeTicks = seriesResult.EpisodeRunTime?.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault
 284
 0285            if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus))
 286            {
 0287                series.Status = seriesStatus;
 288            }
 289
 0290            series.EndDate = seriesResult.LastAirDate;
 0291            series.PremiereDate = seriesResult.FirstAirDate;
 0292            series.ProductionYear = seriesResult.FirstAirDate?.Year;
 293
 0294            var ids = seriesResult.ExternalIds;
 0295            if (ids is not null)
 296            {
 0297                series.TrySetProviderId(MetadataProvider.Imdb, ids.ImdbId);
 0298                series.TrySetProviderId(MetadataProvider.TvRage, ids.TvrageId);
 0299                series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
 300            }
 301
 0302            var contentRatings = seriesResult.ContentRatings?.Results ?? new List<ContentRating>();
 303
 0304            var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, String
 0305            var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.Ordina
 0306            var minimumRelease = contentRatings.FirstOrDefault();
 307
 0308            if (ourRelease?.Rating is not null)
 309            {
 0310                series.OfficialRating = TmdbUtils.BuildParentalRating(preferredCountryCode, ourRelease.Rating);
 311            }
 0312            else if (usRelease?.Rating is not null)
 313            {
 0314                series.OfficialRating = usRelease.Rating;
 315            }
 0316            else if (minimumRelease?.Rating is not null)
 317            {
 0318                series.OfficialRating = minimumRelease.Rating;
 319            }
 320
 0321            if (seriesResult.Videos?.Results is not null)
 322            {
 0323                foreach (var video in seriesResult.Videos.Results)
 324                {
 0325                    if (TmdbUtils.IsTrailerType(video))
 326                    {
 0327                        series.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key);
 328                    }
 329                }
 330            }
 331
 0332            return series;
 333        }
 334
 335        private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
 336        {
 337            var config = Plugin.Instance.Configuration;
 338
 339            if (seriesResult.Credits?.Cast is not null)
 340            {
 341                IEnumerable<Cast> castQuery = seriesResult.Credits.Cast.OrderBy(a => a.Order);
 342
 343                if (config.HideMissingCastMembers)
 344                {
 345                    castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath));
 346                }
 347
 348                foreach (var actor in castQuery.Take(config.MaxCastMembers))
 349                {
 350                    if (string.IsNullOrWhiteSpace(actor.Name))
 351                    {
 352                        continue;
 353                    }
 354
 355                    var personInfo = new PersonInfo
 356                    {
 357                        Name = actor.Name.Trim(),
 358                        Role = actor.Character?.Trim() ?? string.Empty,
 359                        Type = PersonKind.Actor,
 360                        SortOrder = actor.Order,
 361                        // NOTE: Null values are filtered out above
 362                        ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!)
 363                    };
 364
 365                    if (actor.Id > 0)
 366                    {
 367                        personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture))
 368                    }
 369
 370                    yield return personInfo;
 371                }
 372            }
 373
 374            if (seriesResult.Credits?.Crew is not null)
 375            {
 376                var crewQuery = seriesResult.Credits.Crew
 377                    .Select(crewMember => new
 378                    {
 379                        CrewMember = crewMember,
 380                        PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
 381                    })
 382                    .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
 383
 384                if (config.HideMissingCrewMembers)
 385                {
 386                    crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
 387                }
 388
 389                foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
 390                {
 391                    var crewMember = entry.CrewMember;
 392
 393                    if (string.IsNullOrWhiteSpace(crewMember.Name))
 394                    {
 395                        continue;
 396                    }
 397
 398                    var personInfo = new PersonInfo
 399                    {
 400                        Name = crewMember.Name.Trim(),
 401                        Role = crewMember.Job?.Trim() ?? string.Empty,
 402                        Type = entry.PersonType,
 403                        // NOTE: Null values are filtered out above
 404                        ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!)
 405                    };
 406
 407                    if (crewMember.Id > 0)
 408                    {
 409                        personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCult
 410                    }
 411
 412                    yield return personInfo;
 413                }
 414            }
 415        }
 416
 417        /// <inheritdoc />
 418        public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
 419        {
 0420            return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
 421        }
 422    }
 423}