< 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: 568
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

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        {
 8841            _httpClientFactory = httpClientFactory;
 8842            _fileSystem = fileSystem;
 8843            _configurationManager = configurationManager;
 44
 8845            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
 46            // These converters need to take priority
 8847            _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableStringConverter());
 8848            _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableInt32Converter());
 8849        }
 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
 142            var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(f
 143
 144            if (seasonResult?.Episodes is null)
 145            {
 146                return false;
 147            }
 148
 149            RootObject result = null;
 150
 151            if (!string.IsNullOrWhiteSpace(episodeImdbId))
 152            {
 153                foreach (var episode in seasonResult.Episodes)
 154                {
 155                    if (string.Equals(episodeImdbId, episode.imdbID, StringComparison.OrdinalIgnoreCase))
 156                    {
 157                        result = episode;
 158                        break;
 159                    }
 160                }
 161            }
 162
 163            // finally, search by numbers
 164            if (result is null)
 165            {
 166                foreach (var episode in seasonResult.Episodes)
 167                {
 168                    if (episode.Episode == episodeNumber)
 169                    {
 170                        result = episode;
 171                        break;
 172                    }
 173                }
 174            }
 175
 176            if (result is null)
 177            {
 178                return false;
 179            }
 180
 181            var isEnglishRequested = IsConfiguredForEnglish(item, language);
 182            // Only take the name and rating if the user's language is set to English, since Omdb has no localization
 183            if (isEnglishRequested)
 184            {
 185                item.Name = result.Title;
 186
 187                if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
 188                {
 189                    item.OfficialRating = result.Rated;
 190                }
 191            }
 192
 193            if (TryParseYear(result.Year, out var year))
 194            {
 195                item.ProductionYear = year;
 196            }
 197
 198            var tomatoScore = result.GetRottenTomatoScore();
 199
 200            if (tomatoScore.HasValue)
 201            {
 202                item.CriticRating = tomatoScore;
 203            }
 204
 205            if (!string.IsNullOrEmpty(result.imdbVotes)
 206                && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
 207                && voteCount >= 0)
 208            {
 209                // item.VoteCount = voteCount;
 210            }
 211
 212            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 213                && imdbRating >= 0)
 214            {
 215                item.CommunityRating = imdbRating;
 216            }
 217
 218            if (!string.IsNullOrEmpty(result.Website))
 219            {
 220                item.HomePageUrl = result.Website;
 221            }
 222
 223            item.TrySetProviderId(MetadataProvider.Imdb, result.imdbID);
 224
 225            ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
 226
 227            return true;
 228        }
 229
 230        internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
 231        {
 232            var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
 233            var stream = AsyncFile.OpenRead(path);
 234            await using (stream.ConfigureAwait(false))
 235            {
 236                return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).Config
 237            }
 238        }
 239
 240        internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancell
 241        {
 242            var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
 243            var stream = AsyncFile.OpenRead(path);
 244            await using (stream.ConfigureAwait(false))
 245            {
 246                return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).
 247            }
 248        }
 249
 250        /// <summary>Gets OMDB URL.</summary>
 251        /// <param name="query">Appends query string to URL.</param>
 252        /// <returns>OMDB URL with optional query string.</returns>
 253        public static string GetOmdbUrl(string query)
 254        {
 255            const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
 256
 0257            if (string.IsNullOrWhiteSpace(query))
 258            {
 0259                return Url;
 260            }
 261
 0262            return Url + "&" + query;
 263        }
 264
 265        /// <summary>
 266        /// Extract the year from a string.
 267        /// </summary>
 268        /// <param name="input">The input string.</param>
 269        /// <param name="year">The year.</param>
 270        /// <returns>A value indicating whether the input could successfully be parsed as a year.</returns>
 271        public static bool TryParseYear(string input, [NotNullWhen(true)] out int? year)
 272        {
 0273            if (string.IsNullOrEmpty(input))
 274            {
 0275                year = 0;
 0276                return false;
 277            }
 278
 0279            if (int.TryParse(input.AsSpan(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
 280            {
 0281                year = result;
 0282                return true;
 283            }
 284
 0285            year = 0;
 0286            return false;
 287        }
 288
 289        private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
 290        {
 291            if (string.IsNullOrWhiteSpace(imdbId))
 292            {
 293                throw new ArgumentNullException(nameof(imdbId));
 294            }
 295
 296            var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
 297
 298            var path = GetDataFilePath(imdbParam);
 299
 300            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 301
 302            if (fileInfo.Exists)
 303            {
 304                // If it's recent or automatic updates are enabled, don't re-download
 305                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 1)
 306                {
 307                    return path;
 308                }
 309            }
 310            else
 311            {
 312                Directory.CreateDirectory(Path.GetDirectoryName(path));
 313            }
 314
 315            var url = GetOmdbUrl(
 316                string.Format(
 317                    CultureInfo.InvariantCulture,
 318                    "i={0}&plot=short&tomatoes=true&r=json",
 319                    imdbParam));
 320
 321            var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<RootObject>(url
 322            FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaul
 323            await using (jsonFileStream.ConfigureAwait(false))
 324            {
 325                await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).Configu
 326            }
 327
 328            return path;
 329        }
 330
 331        private async Task<string> EnsureSeasonInfo(string seriesImdbId, int seasonId, CancellationToken cancellationTok
 332        {
 333            if (string.IsNullOrWhiteSpace(seriesImdbId))
 334            {
 335                throw new ArgumentException("The series IMDb ID was null or whitespace.", nameof(seriesImdbId));
 336            }
 337
 338            var imdbParam = seriesImdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? seriesImdbId : "tt" + se
 339
 340            var path = GetSeasonFilePath(imdbParam, seasonId);
 341
 342            var fileInfo = _fileSystem.GetFileSystemInfo(path);
 343
 344            if (fileInfo.Exists)
 345            {
 346                // If it's recent or automatic updates are enabled, don't re-download
 347                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 1)
 348                {
 349                    return path;
 350                }
 351            }
 352            else
 353            {
 354                Directory.CreateDirectory(Path.GetDirectoryName(path));
 355            }
 356
 357            var url = GetOmdbUrl(
 358                string.Format(
 359                    CultureInfo.InvariantCulture,
 360                    "i={0}&season={1}&detail=full",
 361                    imdbParam,
 362                    seasonId));
 363
 364            var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<SeasonRootObjec
 365            FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaul
 366            await using (jsonFileStream.ConfigureAwait(false))
 367            {
 368                await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).Configu
 369            }
 370
 371            return path;
 372        }
 373
 374        internal string GetDataFilePath(string imdbId)
 375        {
 0376            ArgumentException.ThrowIfNullOrEmpty(imdbId);
 377
 0378            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
 379
 0380            var filename = string.Format(CultureInfo.InvariantCulture, "{0}.json", imdbId);
 381
 0382            return Path.Combine(dataPath, filename);
 383        }
 384
 385        internal string GetSeasonFilePath(string imdbId, int seasonId)
 386        {
 0387            ArgumentException.ThrowIfNullOrEmpty(imdbId);
 388
 0389            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
 390
 0391            var filename = string.Format(CultureInfo.InvariantCulture, "{0}_season_{1}.json", imdbId, seasonId);
 392
 0393            return Path.Combine(dataPath, filename);
 394        }
 395
 396        private static void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result, bool isEnglishRe
 397            where T : BaseItem
 398        {
 0399            var item = itemResult.Item;
 400
 401            // Grab series genres because IMDb data is better than TVDB. Leave movies alone
 402            // But only do it if English is the preferred language because this data will not be localized
 0403            if (isEnglishRequested && !string.IsNullOrWhiteSpace(result.Genre))
 404            {
 0405                item.Genres = Array.Empty<string>();
 406
 0407                foreach (var genre in result.Genre.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions
 408                {
 0409                    item.AddGenre(genre);
 410                }
 411            }
 412
 0413            item.Overview = result.Plot;
 414
 0415            if (!Plugin.Instance.Configuration.CastAndCrew)
 416            {
 0417                return;
 418            }
 419
 0420            if (!string.IsNullOrWhiteSpace(result.Director))
 421            {
 0422                var person = new PersonInfo
 0423                {
 0424                    Name = result.Director,
 0425                    Type = PersonKind.Director
 0426                };
 427
 0428                itemResult.AddPerson(person);
 429            }
 430
 0431            if (!string.IsNullOrWhiteSpace(result.Writer))
 432            {
 0433                var person = new PersonInfo
 0434                {
 0435                    Name = result.Writer,
 0436                    Type = PersonKind.Writer
 0437                };
 438
 0439                itemResult.AddPerson(person);
 440            }
 441
 0442            if (!string.IsNullOrWhiteSpace(result.Actors))
 443            {
 0444                var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.Trim
 0445                foreach (var actor in actorList)
 446                {
 0447                    var person = new PersonInfo
 0448                    {
 0449                        Name = actor,
 0450                        Type = PersonKind.Actor
 0451                    };
 452
 0453                    itemResult.AddPerson(person);
 454                }
 455            }
 0456        }
 457
 458        private static bool IsConfiguredForEnglish(BaseItem item, string language)
 459        {
 0460            if (string.IsNullOrEmpty(language))
 461            {
 0462                language = item.GetPreferredMetadataLanguage();
 463            }
 464
 465            // The data isn't localized and so can only be used for English users
 0466            return string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
 467        }
 468
 469        internal class SeasonRootObject
 470        {
 471            public string Title { get; set; }
 472
 473            public string seriesID { get; set; }
 474
 475            public int? Season { get; set; }
 476
 477            public int? totalSeasons { get; set; }
 478
 479            public RootObject[] Episodes { get; set; }
 480
 481            public string Response { get; set; }
 482        }
 483
 484        internal class RootObject
 485        {
 486            public string Title { get; set; }
 487
 488            public string Year { get; set; }
 489
 490            public string Rated { get; set; }
 491
 492            public string Released { get; set; }
 493
 494            public string Runtime { get; set; }
 495
 496            public string Genre { get; set; }
 497
 498            public string Director { get; set; }
 499
 500            public string Writer { get; set; }
 501
 502            public string Actors { get; set; }
 503
 504            public string Plot { get; set; }
 505
 506            public string Language { get; set; }
 507
 508            public string Country { get; set; }
 509
 510            public string Awards { get; set; }
 511
 512            public string Poster { get; set; }
 513
 514            public List<OmdbRating> Ratings { get; set; }
 515
 516            public string Metascore { get; set; }
 517
 518            public string imdbRating { get; set; }
 519
 520            public string imdbVotes { get; set; }
 521
 522            public string imdbID { get; set; }
 523
 524            public string Type { get; set; }
 525
 526            public string DVD { get; set; }
 527
 528            public string BoxOffice { get; set; }
 529
 530            public string Production { get; set; }
 531
 532            public string Website { get; set; }
 533
 534            public string Response { get; set; }
 535
 536            public int? Episode { get; set; }
 537
 538            public float? GetRottenTomatoScore()
 539            {
 0540                if (Ratings is not null)
 541                {
 0542                    var rating = Ratings.FirstOrDefault(i => string.Equals(i.Source, "Rotten Tomatoes", StringComparison
 0543                    if (rating?.Value is not null)
 544                    {
 0545                        var value = rating.Value.TrimEnd('%');
 0546                        if (float.TryParse(value, CultureInfo.InvariantCulture, out var score))
 547                        {
 0548                            return score;
 549                        }
 550                    }
 551                }
 552
 0553                return null;
 554            }
 555        }
 556
 557#pragma warning disable CA1034
 558        /// <summary>Describes OMDB rating.</summary>
 559        public class OmdbRating
 560        {
 561            /// <summary>Gets or sets rating source.</summary>
 562            public string Source { get; set; }
 563
 564            /// <summary>Gets or sets rating value.</summary>
 565            public string Value { get; set; }
 566        }
 567    }
 568}