< 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
22%
Covered lines: 46
Uncovered lines: 162
Coverable lines: 208
Total lines: 637
Line coverage: 22.1%
Branch coverage
18%
Covered branches: 24
Total branches: 128
Branch coverage: 18.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 3.7% (7/189) Branch coverage: 0% (0/108) Total lines: 5715/27/2026 - 12:15:38 AM Line coverage: 3.7% (7/189) Branch coverage: 0% (0/110) Total lines: 57110/29/2026 - 12:17:11 AM Line coverage: 22.1% (46/208) Branch coverage: 18.7% (24/128) Total lines: 637 5/16/2026 - 12:15:55 AM Line coverage: 3.7% (7/189) Branch coverage: 0% (0/108) Total lines: 5715/27/2026 - 12:15:38 AM Line coverage: 3.7% (7/189) Branch coverage: 0% (0/110) Total lines: 57110/29/2026 - 12:17:11 AM Line coverage: 22.1% (46/208) Branch coverage: 18.7% (24/128) Total lines: 637

Coverage delta

Coverage delta 19 -19

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
CreateJsonOptions()100%11100%
Fetch()0%506220%
FetchEpisodeData()0%1482380%
GetRootObject()100%210%
GetSeasonRootObject()100%210%
GetOmdbUrl(...)0%620%
TryParseYear(...)0%2040%
EnsureItemInfo()0%7280%
EnsureSeasonInfo()0%7280%
GetDataFilePath(...)100%210%
GetSeasonFilePath(...)100%210%
ParseAdditionalMetadata(...)0%110100%
AddPeople(...)100%1414100%
SplitCredits()80%1010100%
IsNameSuffix(...)100%22100%
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        /// <summary>Generational suffixes that OMDb separates from the name with a comma.</summary>
 131        private static readonly string[] NameSuffixes = ["Jr", "Jnr", "Sr", "Snr", "II", "III", "IV", "V"];
 32
 33        private readonly IFileSystem _fileSystem;
 34        private readonly IServerConfigurationManager _configurationManager;
 35        private readonly IHttpClientFactory _httpClientFactory;
 36        private readonly JsonSerializerOptions _jsonOptions;
 37
 38        /// <summary>Initializes a new instance of the <see cref="OmdbProvider"/> class.</summary>
 39        /// <param name="httpClientFactory">HttpClientFactory to use for calls to OMDB service.</param>
 40        /// <param name="fileSystem">IFileSystem to use for store OMDB data.</param>
 41        /// <param name="configurationManager">IServerConfigurationManager to use.</param>
 42        public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager co
 43        {
 10844            _httpClientFactory = httpClientFactory;
 10845            _fileSystem = fileSystem;
 10846            _configurationManager = configurationManager;
 47
 10848            _jsonOptions = CreateJsonOptions();
 10849        }
 50
 51        /// <summary>Creates the <see cref="JsonSerializerOptions"/> to read OMDB responses with.</summary>
 52        /// <returns>The options.</returns>
 53        internal static JsonSerializerOptions CreateJsonOptions()
 54        {
 16355            var options = new JsonSerializerOptions(JsonDefaults.Options);
 56            // These have to be inserted, appending leaves the default string converter ahead of them
 16357            options.Converters.Insert(0, new JsonOmdbNotAvailableStringConverter());
 16358            options.Converters.Insert(0, new JsonOmdbNotAvailableInt32Converter());
 59
 16360            return options;
 61        }
 62
 63        /// <summary>Fetches data from OMDB service.</summary>
 64        /// <param name="itemResult">Metadata about media item.</param>
 65        /// <param name="imdbId">IMDB ID for media.</param>
 66        /// <param name="language">Media language.</param>
 67        /// <param name="country">Country of origin.</param>
 68        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
 69        /// <typeparam name="T">The first generic type parameter.</typeparam>
 70        /// <returns>Returns a Task object that can be awaited.</returns>
 71        public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, Cancell
 72            where T : BaseItem
 73        {
 074            if (string.IsNullOrWhiteSpace(imdbId))
 75            {
 076                throw new ArgumentNullException(nameof(imdbId));
 77            }
 78
 079            var item = itemResult.Item;
 80
 081            var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
 82
 083            var isEnglishRequested = IsConfiguredForEnglish(item, language);
 84            // Only take the name and rating if the user's language is set to English, since Omdb has no localization
 085            if (isEnglishRequested)
 86            {
 087                item.Name = result.Title;
 88
 089                if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
 90                {
 091                    item.OfficialRating = result.Rated;
 92                }
 93            }
 94
 095            if (TryParseYear(result.Year, out var year))
 96            {
 097                item.ProductionYear = year;
 98            }
 99
 0100            var tomatoScore = result.GetRottenTomatoScore();
 101
 0102            if (tomatoScore.HasValue)
 103            {
 0104                item.CriticRating = tomatoScore;
 105            }
 106
 0107            if (!string.IsNullOrEmpty(result.imdbVotes)
 0108                && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
 0109                && voteCount >= 0)
 110            {
 111                // item.VoteCount = voteCount;
 112            }
 113
 0114            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 0115                && imdbRating >= 0)
 116            {
 0117                item.CommunityRating = imdbRating;
 118            }
 119
 0120            if (!string.IsNullOrEmpty(result.Website))
 121            {
 0122                item.HomePageUrl = result.Website;
 123            }
 124
 0125            if (!string.IsNullOrWhiteSpace(result.imdbID))
 126            {
 0127                item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
 128            }
 129
 0130            ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
 0131        }
 132
 133        /// <summary>Gets data about an episode.</summary>
 134        /// <param name="itemResult">Metadata about episode.</param>
 135        /// <param name="episodeNumber">Episode number.</param>
 136        /// <param name="seasonNumber">Season number.</param>
 137        /// <param name="episodeImdbId">Episode ID.</param>
 138        /// <param name="seriesImdbId">Season ID.</param>
 139        /// <param name="language">Episode language.</param>
 140        /// <param name="country">Country of origin.</param>
 141        /// <param name="cancellationToken">CancellationToken to use for operation.</param>
 142        /// <typeparam name="T">The first generic type parameter.</typeparam>
 143        /// <returns>Whether operation was successful.</returns>
 144        public async Task<bool> FetchEpisodeData<T>(MetadataResult<T> itemResult, int episodeNumber, int seasonNumber, s
 145            where T : BaseItem
 146        {
 0147            if (string.IsNullOrWhiteSpace(seriesImdbId))
 148            {
 0149                throw new ArgumentNullException(nameof(seriesImdbId));
 150            }
 151
 0152            var item = itemResult.Item;
 0153            item.IndexNumber = episodeNumber;
 0154            item.ParentIndexNumber = seasonNumber;
 155
 0156            var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(f
 157
 0158            if (seasonResult?.Episodes is null)
 159            {
 0160                return false;
 161            }
 162
 0163            RootObject result = null;
 164
 0165            if (!string.IsNullOrWhiteSpace(episodeImdbId))
 166            {
 0167                foreach (var episode in seasonResult.Episodes)
 168                {
 0169                    if (string.Equals(episodeImdbId, episode.imdbID, StringComparison.OrdinalIgnoreCase))
 170                    {
 0171                        result = episode;
 0172                        break;
 173                    }
 174                }
 175            }
 176
 177            // finally, search by numbers
 0178            if (result is null)
 179            {
 0180                foreach (var episode in seasonResult.Episodes)
 181                {
 0182                    if (episode.Episode == episodeNumber)
 183                    {
 0184                        result = episode;
 0185                        break;
 186                    }
 187                }
 188            }
 189
 0190            if (result is null)
 191            {
 0192                return false;
 193            }
 194
 0195            var isEnglishRequested = IsConfiguredForEnglish(item, language);
 196            // Only take the name and rating if the user's language is set to English, since Omdb has no localization
 0197            if (isEnglishRequested)
 198            {
 0199                item.Name = result.Title;
 200
 0201                if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
 202                {
 0203                    item.OfficialRating = result.Rated;
 204                }
 205            }
 206
 0207            if (TryParseYear(result.Year, out var year))
 208            {
 0209                item.ProductionYear = year;
 210            }
 211
 0212            var tomatoScore = result.GetRottenTomatoScore();
 213
 0214            if (tomatoScore.HasValue)
 215            {
 0216                item.CriticRating = tomatoScore;
 217            }
 218
 0219            if (!string.IsNullOrEmpty(result.imdbVotes)
 0220                && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
 0221                && voteCount >= 0)
 222            {
 223                // item.VoteCount = voteCount;
 224            }
 225
 0226            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 0227                && imdbRating >= 0)
 228            {
 0229                item.CommunityRating = imdbRating;
 230            }
 231
 0232            if (!string.IsNullOrEmpty(result.Website))
 233            {
 0234                item.HomePageUrl = result.Website;
 235            }
 236
 0237            item.TrySetProviderId(MetadataProvider.Imdb, result.imdbID);
 238
 0239            ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
 240
 0241            return true;
 0242        }
 243
 244        internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
 245        {
 0246            var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
 0247            var stream = AsyncFile.OpenRead(path);
 0248            await using (stream.ConfigureAwait(false))
 249            {
 0250                return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).Config
 251            }
 0252        }
 253
 254        internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancell
 255        {
 0256            var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
 0257            var stream = AsyncFile.OpenRead(path);
 0258            await using (stream.ConfigureAwait(false))
 259            {
 0260                return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).
 261            }
 0262        }
 263
 264        /// <summary>Gets OMDB URL.</summary>
 265        /// <param name="query">Appends query string to URL.</param>
 266        /// <returns>OMDB URL with optional query string.</returns>
 267        public static string GetOmdbUrl(string query)
 268        {
 269            const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
 270
 0271            if (string.IsNullOrWhiteSpace(query))
 272            {
 0273                return Url;
 274            }
 275
 0276            return Url + "&" + query;
 277        }
 278
 279        /// <summary>
 280        /// Extract the year from a string.
 281        /// </summary>
 282        /// <param name="input">The input string.</param>
 283        /// <param name="year">The year.</param>
 284        /// <returns>A value indicating whether the input could successfully be parsed as a year.</returns>
 285        public static bool TryParseYear(string input, [NotNullWhen(true)] out int? year)
 286        {
 0287            if (string.IsNullOrEmpty(input))
 288            {
 0289                year = 0;
 0290                return false;
 291            }
 292
 0293            if (int.TryParse(input.AsSpan(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
 294            {
 0295                year = result;
 0296                return true;
 297            }
 298
 0299            year = 0;
 0300            return false;
 301        }
 302
 303        private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
 304        {
 0305            if (string.IsNullOrWhiteSpace(imdbId))
 306            {
 0307                throw new ArgumentNullException(nameof(imdbId));
 308            }
 309
 0310            var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
 311
 0312            var path = GetDataFilePath(imdbParam);
 313
 0314            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 315
 0316            if (fileInfo.Exists)
 317            {
 318                // If it's recent or automatic updates are enabled, don't re-download
 0319                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 1)
 320                {
 0321                    return path;
 322                }
 323            }
 324            else
 325            {
 0326                Directory.CreateDirectory(Path.GetDirectoryName(path));
 327            }
 328
 0329            var url = GetOmdbUrl(
 0330                string.Format(
 0331                    CultureInfo.InvariantCulture,
 0332                    "i={0}&plot=short&tomatoes=true&r=json",
 0333                    imdbParam));
 334
 0335            var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<RootObject>(url
 0336            FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaul
 0337            await using (jsonFileStream.ConfigureAwait(false))
 338            {
 0339                await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).Configu
 340            }
 341
 0342            return path;
 0343        }
 344
 345        private async Task<string> EnsureSeasonInfo(string seriesImdbId, int seasonId, CancellationToken cancellationTok
 346        {
 0347            if (string.IsNullOrWhiteSpace(seriesImdbId))
 348            {
 0349                throw new ArgumentException("The series IMDb ID was null or whitespace.", nameof(seriesImdbId));
 350            }
 351
 0352            var imdbParam = seriesImdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? seriesImdbId : "tt" + se
 353
 0354            var path = GetSeasonFilePath(imdbParam, seasonId);
 355
 0356            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 357
 0358            if (fileInfo.Exists)
 359            {
 360                // If it's recent or automatic updates are enabled, don't re-download
 0361                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 1)
 362                {
 0363                    return path;
 364                }
 365            }
 366            else
 367            {
 0368                Directory.CreateDirectory(Path.GetDirectoryName(path));
 369            }
 370
 0371            var url = GetOmdbUrl(
 0372                string.Format(
 0373                    CultureInfo.InvariantCulture,
 0374                    "i={0}&season={1}&detail=full",
 0375                    imdbParam,
 0376                    seasonId));
 377
 0378            var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<SeasonRootObjec
 0379            FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaul
 0380            await using (jsonFileStream.ConfigureAwait(false))
 381            {
 0382                await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).Configu
 383            }
 384
 0385            return path;
 0386        }
 387
 388        internal string GetDataFilePath(string imdbId)
 389        {
 0390            ArgumentException.ThrowIfNullOrEmpty(imdbId);
 391
 0392            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
 393
 0394            var filename = string.Format(CultureInfo.InvariantCulture, "{0}.json", imdbId);
 395
 0396            return Path.Combine(dataPath, filename);
 397        }
 398
 399        internal string GetSeasonFilePath(string imdbId, int seasonId)
 400        {
 0401            ArgumentException.ThrowIfNullOrEmpty(imdbId);
 402
 0403            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
 404
 0405            var filename = string.Format(CultureInfo.InvariantCulture, "{0}_season_{1}.json", imdbId, seasonId);
 406
 0407            return Path.Combine(dataPath, filename);
 408        }
 409
 410        private static void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result, bool isEnglishRe
 411            where T : BaseItem
 412        {
 0413            var item = itemResult.Item;
 414
 415            // Grab series genres because IMDb data is better than TVDB. Leave movies alone
 416            // But only do it if English is the preferred language because this data will not be localized
 0417            if (isEnglishRequested && !string.IsNullOrWhiteSpace(result.Genre))
 418            {
 0419                item.Genres = Array.Empty<string>();
 420
 0421                foreach (var genre in result.Genre.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions
 422                {
 0423                    item.AddGenre(genre);
 424                }
 425            }
 426
 0427            item.Overview = result.Plot;
 0428            item.OriginalLanguage = result.Language?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptio
 429
 0430            if (!Plugin.Instance.Configuration.CastAndCrew)
 431            {
 0432                return;
 433            }
 434
 0435            AddPeople(itemResult, result.Director, PersonKind.Director);
 0436            AddPeople(itemResult, result.Writer, PersonKind.Writer);
 0437            AddPeople(itemResult, result.Actors, PersonKind.Actor);
 0438        }
 439
 440        /// <summary>Adds the people from a comma separated OMDb credit list.</summary>
 441        /// <typeparam name="T">The item type.</typeparam>
 442        /// <param name="itemResult">The metadata result to add the people to.</param>
 443        /// <param name="credits">The comma separated OMDb credit list.</param>
 444        /// <param name="type">The kind of person each credit describes.</param>
 445        internal static void AddPeople<T>(MetadataResult<T> itemResult, string credits, PersonKind type)
 446            where T : BaseItem
 447        {
 14448            if (string.IsNullOrWhiteSpace(credits))
 449            {
 3450                return;
 451            }
 452
 11453            var names = new List<string>();
 454
 74455            foreach (var credit in SplitCredits(credits))
 456            {
 457                // OMDb annotates the credited role in parentheses, e.g. "Mari Okada (screenplay)". The same
 458                // person can be credited more than once this way, so strip it and let AddPerson deduplicate.
 26459                var name = credit;
 26460                var annotation = name.IndexOf('(', StringComparison.Ordinal);
 26461                if (annotation >= 0)
 462                {
 9463                    name = name[..annotation];
 464                }
 465
 26466                name = name.Trim();
 26467                if (name.Length == 0)
 468                {
 469                    continue;
 470                }
 471
 472                // A generational suffix is separated from the name it belongs to by the same comma the list
 473                // uses, e.g. "Jack Salvatore, Jr.", so it has to be joined back instead of becoming a credit.
 25474                if (names.Count > 0 && IsNameSuffix(name))
 475                {
 4476                    names[^1] = names[^1] + ", " + name;
 4477                    continue;
 478                }
 479
 21480                names.Add(name);
 481            }
 482
 64483            foreach (var name in names)
 484            {
 21485                itemResult.AddPerson(new PersonInfo
 21486                {
 21487                    Name = name,
 21488                    Type = type
 21489                });
 490            }
 11491        }
 492
 493        // Only the commas between credits, never one inside an annotation: "Jerry Siegel (created by:
 494        // Superman, Superboy)" is one credit, and splitting it blindly invents a person called "Superboy)".
 495        private static IEnumerable<string> SplitCredits(string credits)
 496        {
 11497            var depth = 0;
 11498            var start = 0;
 499
 890500            for (var i = 0; i < credits.Length; i++)
 501            {
 434502                switch (credits[i])
 503                {
 504                    case '(':
 9505                        depth++;
 9506                        break;
 507                    case ')':
 9508                        depth = Math.Max(0, depth - 1);
 9509                        break;
 16510                    case ',' when depth == 0:
 15511                        yield return credits[start..i];
 15512                        start = i + 1;
 513                        break;
 514                }
 515            }
 516
 11517            yield return credits[start..];
 11518        }
 519
 520        private static bool IsNameSuffix(string value)
 521        {
 15522            var suffix = value.EndsWith('.') ? value[..^1] : value;
 523
 15524            return NameSuffixes.Contains(suffix, StringComparer.OrdinalIgnoreCase);
 525        }
 526
 527        private static bool IsConfiguredForEnglish(BaseItem item, string language)
 528        {
 0529            if (string.IsNullOrEmpty(language))
 530            {
 0531                language = item.GetPreferredMetadataLanguage();
 532            }
 533
 534            // The data isn't localized and so can only be used for English users
 0535            return string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
 536        }
 537
 538        internal class SeasonRootObject
 539        {
 540            public string Title { get; set; }
 541
 542            public string seriesID { get; set; }
 543
 544            public int? Season { get; set; }
 545
 546            public int? totalSeasons { get; set; }
 547
 548            public RootObject[] Episodes { get; set; }
 549
 550            public string Response { get; set; }
 551        }
 552
 553        internal class RootObject
 554        {
 555            public string Title { get; set; }
 556
 557            public string Year { get; set; }
 558
 559            public string Rated { get; set; }
 560
 561            public string Released { get; set; }
 562
 563            public string Runtime { get; set; }
 564
 565            public string Genre { get; set; }
 566
 567            public string Director { get; set; }
 568
 569            public string Writer { get; set; }
 570
 571            public string Actors { get; set; }
 572
 573            public string Plot { get; set; }
 574
 575            public string Language { get; set; }
 576
 577            public string Country { get; set; }
 578
 579            public string Awards { get; set; }
 580
 581            public string Poster { get; set; }
 582
 583            public List<OmdbRating> Ratings { get; set; }
 584
 585            public string Metascore { get; set; }
 586
 587            public string imdbRating { get; set; }
 588
 589            public string imdbVotes { get; set; }
 590
 591            public string imdbID { get; set; }
 592
 593            public string Type { get; set; }
 594
 595            public string DVD { get; set; }
 596
 597            public string BoxOffice { get; set; }
 598
 599            public string Production { get; set; }
 600
 601            public string Website { get; set; }
 602
 603            public string Response { get; set; }
 604
 605            public int? Episode { get; set; }
 606
 607            public float? GetRottenTomatoScore()
 608            {
 0609                if (Ratings is not null)
 610                {
 0611                    var rating = Ratings.FirstOrDefault(i => string.Equals(i.Source, "Rotten Tomatoes", StringComparison
 0612                    if (rating?.Value is not null)
 613                    {
 0614                        var value = rating.Value.TrimEnd('%');
 0615                        if (float.TryParse(value, CultureInfo.InvariantCulture, out var score))
 616                        {
 0617                            return score;
 618                        }
 619                    }
 620                }
 621
 0622                return null;
 623            }
 624        }
 625
 626#pragma warning disable CA1034
 627        /// <summary>Describes OMDB rating.</summary>
 628        public class OmdbRating
 629        {
 630            /// <summary>Gets or sets rating source.</summary>
 631            public string Source { get; set; }
 632
 633            /// <summary>Gets or sets rating value.</summary>
 634            public string Value { get; set; }
 635        }
 636    }
 637}