< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Plugins.Omdb.OmdbProvider
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
Line coverage
10%
Covered lines: 7
Uncovered lines: 61
Coverable lines: 68
Total lines: 570
Line coverage: 10.2%
Branch coverage
0%
Covered branches: 0
Total branches: 32
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/14/2025 - 12:11:05 AM Line coverage: 10.2% (7/68) Branch coverage: 0% (0/32) Total lines: 56810/28/2025 - 12:11:27 AM Line coverage: 10.2% (7/68) Branch coverage: 0% (0/32) Total lines: 570 8/14/2025 - 12:11:05 AM Line coverage: 10.2% (7/68) Branch coverage: 0% (0/32) Total lines: 56810/28/2025 - 12:11:27 AM Line coverage: 10.2% (7/68) Branch coverage: 0% (0/32) Total lines: 570

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetOmdbUrl(...)0%620%
TryParseYear(...)0%2040%
GetDataFilePath(...)100%210%
GetSeasonFilePath(...)100%210%
ParseAdditionalMetadata(...)0%272160%
IsConfiguredForEnglish(...)0%620%
GetRottenTomatoScore()0%7280%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS159, SA1300
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Net.Http;
 12using System.Net.Http.Json;
 13using System.Text.Json;
 14using System.Threading;
 15using System.Threading.Tasks;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Extensions.Json;
 18using MediaBrowser.Common.Net;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Entities;
 21using MediaBrowser.Controller.Providers;
 22using MediaBrowser.Model.Entities;
 23using MediaBrowser.Model.IO;
 24
 25namespace MediaBrowser.Providers.Plugins.Omdb
 26{
 27    /// <summary>Provider for OMDB service.</summary>
 28    public class OmdbProvider
 29    {
 30        private readonly IFileSystem _fileSystem;
 31        private readonly IServerConfigurationManager _configurationManager;
 32        private readonly IHttpClientFactory _httpClientFactory;
 33        private readonly JsonSerializerOptions _jsonOptions;
 34
 35        /// <summary>Initializes a new instance of the <see cref="OmdbProvider"/> class.</summary>
 36        /// <param name="httpClientFactory">HttpClientFactory to use for calls to OMDB service.</param>
 37        /// <param name="fileSystem">IFileSystem to use for store OMDB data.</param>
 38        /// <param name="configurationManager">IServerConfigurationManager to use.</param>
 39        public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager co
 40        {
 8441            _httpClientFactory = httpClientFactory;
 8442            _fileSystem = fileSystem;
 8443            _configurationManager = configurationManager;
 44
 8445            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
 46            // These converters need to take priority
 8447            _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableStringConverter());
 8448            _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableInt32Converter());
 8449        }
 50
 51        /// <summary>Fetches data from OMDB service.</summary>
 52        /// <param name="itemResult">Metadata about media item.</param>
 53        /// <param name="imdbId">IMDB ID for media.</param>
 54        /// <param name="language">Media language.</param>
 55        /// <param name="country">Country of origin.</param>
 56        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
 57        /// <typeparam name="T">The first generic type parameter.</typeparam>
 58        /// <returns>Returns a Task object that can be awaited.</returns>
 59        public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, Cancell
 60            where T : BaseItem
 61        {
 62            if (string.IsNullOrWhiteSpace(imdbId))
 63            {
 64                throw new ArgumentNullException(nameof(imdbId));
 65            }
 66
 67            var item = itemResult.Item;
 68
 69            var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
 70
 71            var isEnglishRequested = IsConfiguredForEnglish(item, language);
 72            // Only take the name and rating if the user's language is set to English, since Omdb has no localization
 73            if (isEnglishRequested)
 74            {
 75                item.Name = result.Title;
 76
 77                if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
 78                {
 79                    item.OfficialRating = result.Rated;
 80                }
 81            }
 82
 83            if (TryParseYear(result.Year, out var year))
 84            {
 85                item.ProductionYear = year;
 86            }
 87
 88            var tomatoScore = result.GetRottenTomatoScore();
 89
 90            if (tomatoScore.HasValue)
 91            {
 92                item.CriticRating = tomatoScore;
 93            }
 94
 95            if (!string.IsNullOrEmpty(result.imdbVotes)
 96                && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
 97                && voteCount >= 0)
 98            {
 99                // item.VoteCount = voteCount;
 100            }
 101
 102            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 103                && imdbRating >= 0)
 104            {
 105                item.CommunityRating = imdbRating;
 106            }
 107
 108            if (!string.IsNullOrEmpty(result.Website))
 109            {
 110                item.HomePageUrl = result.Website;
 111            }
 112
 113            if (!string.IsNullOrWhiteSpace(result.imdbID))
 114            {
 115                item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
 116            }
 117
 118            ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
 119        }
 120
 121        /// <summary>Gets data about an episode.</summary>
 122        /// <param name="itemResult">Metadata about episode.</param>
 123        /// <param name="episodeNumber">Episode number.</param>
 124        /// <param name="seasonNumber">Season number.</param>
 125        /// <param name="episodeImdbId">Episode ID.</param>
 126        /// <param name="seriesImdbId">Season ID.</param>
 127        /// <param name="language">Episode language.</param>
 128        /// <param name="country">Country of origin.</param>
 129        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
 130        /// <typeparam name="T">The first generic type parameter.</typeparam>
 131        /// <returns>Whether operation was successful.</returns>
 132        public async Task<bool> FetchEpisodeData<T>(MetadataResult<T> itemResult, int episodeNumber, int seasonNumber, s
 133            where T : BaseItem
 134        {
 135            if (string.IsNullOrWhiteSpace(seriesImdbId))
 136            {
 137                throw new ArgumentNullException(nameof(seriesImdbId));
 138            }
 139
 140            var item = itemResult.Item;
 141            item.IndexNumber = episodeNumber;
 142            item.ParentIndexNumber = seasonNumber;
 143
 144            var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(f
 145
 146            if (seasonResult?.Episodes is null)
 147            {
 148                return false;
 149            }
 150
 151            RootObject result = null;
 152
 153            if (!string.IsNullOrWhiteSpace(episodeImdbId))
 154            {
 155                foreach (var episode in seasonResult.Episodes)
 156                {
 157                    if (string.Equals(episodeImdbId, episode.imdbID, StringComparison.OrdinalIgnoreCase))
 158                    {
 159                        result = episode;
 160                        break;
 161                    }
 162                }
 163            }
 164
 165            // finally, search by numbers
 166            if (result is null)
 167            {
 168                foreach (var episode in seasonResult.Episodes)
 169                {
 170                    if (episode.Episode == episodeNumber)
 171                    {
 172                        result = episode;
 173                        break;
 174                    }
 175                }
 176            }
 177
 178            if (result is null)
 179            {
 180                return false;
 181            }
 182
 183            var isEnglishRequested = IsConfiguredForEnglish(item, language);
 184            // Only take the name and rating if the user's language is set to English, since Omdb has no localization
 185            if (isEnglishRequested)
 186            {
 187                item.Name = result.Title;
 188
 189                if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
 190                {
 191                    item.OfficialRating = result.Rated;
 192                }
 193            }
 194
 195            if (TryParseYear(result.Year, out var year))
 196            {
 197                item.ProductionYear = year;
 198            }
 199
 200            var tomatoScore = result.GetRottenTomatoScore();
 201
 202            if (tomatoScore.HasValue)
 203            {
 204                item.CriticRating = tomatoScore;
 205            }
 206
 207            if (!string.IsNullOrEmpty(result.imdbVotes)
 208                && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
 209                && voteCount >= 0)
 210            {
 211                // item.VoteCount = voteCount;
 212            }
 213
 214            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 215                && imdbRating >= 0)
 216            {
 217                item.CommunityRating = imdbRating;
 218            }
 219
 220            if (!string.IsNullOrEmpty(result.Website))
 221            {
 222                item.HomePageUrl = result.Website;
 223            }
 224
 225            item.TrySetProviderId(MetadataProvider.Imdb, result.imdbID);
 226
 227            ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
 228
 229            return true;
 230        }
 231
 232        internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
 233        {
 234            var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
 235            var stream = AsyncFile.OpenRead(path);
 236            await using (stream.ConfigureAwait(false))
 237            {
 238                return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).Config
 239            }
 240        }
 241
 242        internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancell
 243        {
 244            var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
 245            var stream = AsyncFile.OpenRead(path);
 246            await using (stream.ConfigureAwait(false))
 247            {
 248                return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).
 249            }
 250        }
 251
 252        /// <summary>Gets OMDB URL.</summary>
 253        /// <param name="query">Appends query string to URL.</param>
 254        /// <returns>OMDB URL with optional query string.</returns>
 255        public static string GetOmdbUrl(string query)
 256        {
 257            const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
 258
 0259            if (string.IsNullOrWhiteSpace(query))
 260            {
 0261                return Url;
 262            }
 263
 0264            return Url + "&" + query;
 265        }
 266
 267        /// <summary>
 268        /// Extract the year from a string.
 269        /// </summary>
 270        /// <param name="input">The input string.</param>
 271        /// <param name="year">The year.</param>
 272        /// <returns>A value indicating whether the input could successfully be parsed as a year.</returns>
 273        public static bool TryParseYear(string input, [NotNullWhen(true)] out int? year)
 274        {
 0275            if (string.IsNullOrEmpty(input))
 276            {
 0277                year = 0;
 0278                return false;
 279            }
 280
 0281            if (int.TryParse(input.AsSpan(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
 282            {
 0283                year = result;
 0284                return true;
 285            }
 286
 0287            year = 0;
 0288            return false;
 289        }
 290
 291        private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
 292        {
 293            if (string.IsNullOrWhiteSpace(imdbId))
 294            {
 295                throw new ArgumentNullException(nameof(imdbId));
 296            }
 297
 298            var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
 299
 300            var path = GetDataFilePath(imdbParam);
 301
 302            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 303
 304            if (fileInfo.Exists)
 305            {
 306                // If it's recent or automatic updates are enabled, don't re-download
 307                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 1)
 308                {
 309                    return path;
 310                }
 311            }
 312            else
 313            {
 314                Directory.CreateDirectory(Path.GetDirectoryName(path));
 315            }
 316
 317            var url = GetOmdbUrl(
 318                string.Format(
 319                    CultureInfo.InvariantCulture,
 320                    "i={0}&plot=short&tomatoes=true&r=json",
 321                    imdbParam));
 322
 323            var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<RootObject>(url
 324            FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaul
 325            await using (jsonFileStream.ConfigureAwait(false))
 326            {
 327                await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).Configu
 328            }
 329
 330            return path;
 331        }
 332
 333        private async Task<string> EnsureSeasonInfo(string seriesImdbId, int seasonId, CancellationToken cancellationTok
 334        {
 335            if (string.IsNullOrWhiteSpace(seriesImdbId))
 336            {
 337                throw new ArgumentException("The series IMDb ID was null or whitespace.", nameof(seriesImdbId));
 338            }
 339
 340            var imdbParam = seriesImdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? seriesImdbId : "tt" + se
 341
 342            var path = GetSeasonFilePath(imdbParam, seasonId);
 343
 344            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 345
 346            if (fileInfo.Exists)
 347            {
 348                // If it's recent or automatic updates are enabled, don't re-download
 349                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 1)
 350                {
 351                    return path;
 352                }
 353            }
 354            else
 355            {
 356                Directory.CreateDirectory(Path.GetDirectoryName(path));
 357            }
 358
 359            var url = GetOmdbUrl(
 360                string.Format(
 361                    CultureInfo.InvariantCulture,
 362                    "i={0}&season={1}&detail=full",
 363                    imdbParam,
 364                    seasonId));
 365
 366            var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<SeasonRootObjec
 367            FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaul
 368            await using (jsonFileStream.ConfigureAwait(false))
 369            {
 370                await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).Configu
 371            }
 372
 373            return path;
 374        }
 375
 376        internal string GetDataFilePath(string imdbId)
 377        {
 0378            ArgumentException.ThrowIfNullOrEmpty(imdbId);
 379
 0380            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
 381
 0382            var filename = string.Format(CultureInfo.InvariantCulture, "{0}.json", imdbId);
 383
 0384            return Path.Combine(dataPath, filename);
 385        }
 386
 387        internal string GetSeasonFilePath(string imdbId, int seasonId)
 388        {
 0389            ArgumentException.ThrowIfNullOrEmpty(imdbId);
 390
 0391            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
 392
 0393            var filename = string.Format(CultureInfo.InvariantCulture, "{0}_season_{1}.json", imdbId, seasonId);
 394
 0395            return Path.Combine(dataPath, filename);
 396        }
 397
 398        private static void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result, bool isEnglishRe
 399            where T : BaseItem
 400        {
 0401            var item = itemResult.Item;
 402
 403            // Grab series genres because IMDb data is better than TVDB. Leave movies alone
 404            // But only do it if English is the preferred language because this data will not be localized
 0405            if (isEnglishRequested && !string.IsNullOrWhiteSpace(result.Genre))
 406            {
 0407                item.Genres = Array.Empty<string>();
 408
 0409                foreach (var genre in result.Genre.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions
 410                {
 0411                    item.AddGenre(genre);
 412                }
 413            }
 414
 0415            item.Overview = result.Plot;
 416
 0417            if (!Plugin.Instance.Configuration.CastAndCrew)
 418            {
 0419                return;
 420            }
 421
 0422            if (!string.IsNullOrWhiteSpace(result.Director))
 423            {
 0424                var person = new PersonInfo
 0425                {
 0426                    Name = result.Director.Trim(),
 0427                    Type = PersonKind.Director
 0428                };
 429
 0430                itemResult.AddPerson(person);
 431            }
 432
 0433            if (!string.IsNullOrWhiteSpace(result.Writer))
 434            {
 0435                var person = new PersonInfo
 0436                {
 0437                    Name = result.Writer.Trim(),
 0438                    Type = PersonKind.Writer
 0439                };
 440
 0441                itemResult.AddPerson(person);
 442            }
 443
 0444            if (!string.IsNullOrWhiteSpace(result.Actors))
 445            {
 0446                var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.Trim
 0447                foreach (var actor in actorList)
 448                {
 0449                    var person = new PersonInfo
 0450                    {
 0451                        Name = actor,
 0452                        Type = PersonKind.Actor
 0453                    };
 454
 0455                    itemResult.AddPerson(person);
 456                }
 457            }
 0458        }
 459
 460        private static bool IsConfiguredForEnglish(BaseItem item, string language)
 461        {
 0462            if (string.IsNullOrEmpty(language))
 463            {
 0464                language = item.GetPreferredMetadataLanguage();
 465            }
 466
 467            // The data isn't localized and so can only be used for English users
 0468            return string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
 469        }
 470
 471        internal class SeasonRootObject
 472        {
 473            public string Title { get; set; }
 474
 475            public string seriesID { get; set; }
 476
 477            public int? Season { get; set; }
 478
 479            public int? totalSeasons { get; set; }
 480
 481            public RootObject[] Episodes { get; set; }
 482
 483            public string Response { get; set; }
 484        }
 485
 486        internal class RootObject
 487        {
 488            public string Title { get; set; }
 489
 490            public string Year { get; set; }
 491
 492            public string Rated { get; set; }
 493
 494            public string Released { get; set; }
 495
 496            public string Runtime { get; set; }
 497
 498            public string Genre { get; set; }
 499
 500            public string Director { get; set; }
 501
 502            public string Writer { get; set; }
 503
 504            public string Actors { get; set; }
 505
 506            public string Plot { get; set; }
 507
 508            public string Language { get; set; }
 509
 510            public string Country { get; set; }
 511
 512            public string Awards { get; set; }
 513
 514            public string Poster { get; set; }
 515
 516            public List<OmdbRating> Ratings { get; set; }
 517
 518            public string Metascore { get; set; }
 519
 520            public string imdbRating { get; set; }
 521
 522            public string imdbVotes { get; set; }
 523
 524            public string imdbID { get; set; }
 525
 526            public string Type { get; set; }
 527
 528            public string DVD { get; set; }
 529
 530            public string BoxOffice { get; set; }
 531
 532            public string Production { get; set; }
 533
 534            public string Website { get; set; }
 535
 536            public string Response { get; set; }
 537
 538            public int? Episode { get; set; }
 539
 540            public float? GetRottenTomatoScore()
 541            {
 0542                if (Ratings is not null)
 543                {
 0544                    var rating = Ratings.FirstOrDefault(i => string.Equals(i.Source, "Rotten Tomatoes", StringComparison
 0545                    if (rating?.Value is not null)
 546                    {
 0547                        var value = rating.Value.TrimEnd('%');
 0548                        if (float.TryParse(value, CultureInfo.InvariantCulture, out var score))
 549                        {
 0550                            return score;
 551                        }
 552                    }
 553                }
 554
 0555                return null;
 556            }
 557        }
 558
 559#pragma warning disable CA1034
 560        /// <summary>Describes OMDB rating.</summary>
 561        public class OmdbRating
 562        {
 563            /// <summary>Gets or sets rating source.</summary>
 564            public string Source { get; set; }
 565
 566            /// <summary>Gets or sets rating value.</summary>
 567            public string Value { get; set; }
 568        }
 569    }
 570}