From 41b9ce56efb4f4ff013f7d4d7aa30a4c6dca7789 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 9 Feb 2014 01:08:10 -0500 Subject: [PATCH] added series-level movie db support --- MediaBrowser.Api/Images/RemoteImageService.cs | 8 +- .../Entities/MetadataProviders.cs | 3 +- .../MediaBrowser.Providers.csproj | 2 + .../Movies/GenericMovieDbInfo.cs | 2 - .../Movies/MovieDbImageProvider.cs | 1 - .../Movies/MovieDbSearch.cs | 5 + .../TV/MovieDbSeriesImageProvider.cs | 211 +++++++++ .../TV/MovieDbSeriesProvider.cs | 442 ++++++++++++++++++ 8 files changed, 666 insertions(+), 8 deletions(-) create mode 100644 MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs create mode 100644 MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index 6058733222..895904da76 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -145,8 +145,8 @@ namespace MediaBrowser.Api.Images [Api(Description = "Gets a remote image")] public class GetRemoteImage { - [ApiMember(Name = "Url", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Url { get; set; } + [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string ImageUrl { get; set; } } public class RemoteImageService : BaseApiService @@ -303,7 +303,7 @@ namespace MediaBrowser.Api.Images /// Task{System.Object}. private async Task GetRemoteImage(GetRemoteImage request) { - var urlHash = request.Url.GetMD5(); + var urlHash = request.ImageUrl.GetMD5(); var pointerCachePath = GetFullCachePath(urlHash.ToString()); string contentPath; @@ -325,7 +325,7 @@ namespace MediaBrowser.Api.Images // Means the file isn't cached yet } - await DownloadImage(request.Url, urlHash, pointerCachePath).ConfigureAwait(false); + await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false); // Read the pointer file again using (var reader = new StreamReader(pointerCachePath)) diff --git a/MediaBrowser.Model/Entities/MetadataProviders.cs b/MediaBrowser.Model/Entities/MetadataProviders.cs index 5f55e12874..e021ab46a4 100644 --- a/MediaBrowser.Model/Entities/MetadataProviders.cs +++ b/MediaBrowser.Model/Entities/MetadataProviders.cs @@ -37,6 +37,7 @@ namespace MediaBrowser.Model.Entities MusicBrainzReleaseGroup, Zap2It, NesBox, - NesBoxRom + NesBoxRom, + TvRageSeries } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index c0373a2e8e..efe2b05d11 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -171,6 +171,8 @@ + + diff --git a/MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs b/MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs index 45d1eb5318..44e2beb7f3 100644 --- a/MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs +++ b/MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -13,7 +12,6 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using PersonInfo = MediaBrowser.Controller.Entities.PersonInfo; namespace MediaBrowser.Providers.Movies { diff --git a/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs b/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs index db34688165..5699093e48 100644 --- a/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs @@ -1,6 +1,5 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Providers/Movies/MovieDbSearch.cs b/MediaBrowser.Providers/Movies/MovieDbSearch.cs index 5b178b0a90..5a2b865f54 100644 --- a/MediaBrowser.Providers/Movies/MovieDbSearch.cs +++ b/MediaBrowser.Providers/Movies/MovieDbSearch.cs @@ -29,6 +29,11 @@ namespace MediaBrowser.Providers.Movies _json = json; } + public Task FindSeriesId(ItemLookupInfo idInfo, CancellationToken cancellationToken) + { + return FindId(idInfo, "tv", cancellationToken); + } + public Task FindMovieId(ItemLookupInfo idInfo, CancellationToken cancellationToken) { return FindId(idInfo, "movie", cancellationToken); diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs new file mode 100644 index 0000000000..3a98d95e49 --- /dev/null +++ b/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs @@ -0,0 +1,211 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + public class MovieDbSeriesImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + + public MovieDbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient) + { + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "TheMovieDb"; } + } + + public bool Supports(IHasImages item) + { + return item is Series; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Backdrop + }; + } + + public async Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) + { + var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); + + return images.Where(i => i.Type == imageType); + } + + public async Task> GetAllImages(IHasImages item, CancellationToken cancellationToken) + { + var list = new List(); + + var results = await FetchImages((BaseItem)item, _jsonSerializer, cancellationToken).ConfigureAwait(false); + + if (results == null) + { + return list; + } + + var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + + var tmdbImageUrl = tmdbSettings.images.base_url + "original"; + + list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo + { + Url = tmdbImageUrl + i.file_path, + CommunityRating = i.vote_average, + VoteCount = i.vote_count, + Width = i.width, + Height = i.height, + Language = i.iso_639_1, + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + })); + + list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo + { + Url = tmdbImageUrl + i.file_path, + CommunityRating = i.vote_average, + VoteCount = i.vote_count, + Width = i.width, + Height = i.height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + })); + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + return list.OrderByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0) + .ToList(); + } + + /// + /// Gets the posters. + /// + /// The images. + private IEnumerable GetPosters(MovieDbSeriesProvider.Images images) + { + return images.posters ?? new List(); + } + + /// + /// Gets the backdrops. + /// + /// The images. + private IEnumerable GetBackdrops(MovieDbSeriesProvider.Images images) + { + var eligibleBackdrops = images.backdrops == null ? new List() : + images.backdrops + .ToList(); + + return eligibleBackdrops.OrderByDescending(i => i.vote_average) + .ThenByDescending(i => i.vote_count); + } + + /// + /// Fetches the images. + /// + /// The item. + /// The json serializer. + /// The cancellation token. + /// Task{MovieImages}. + private async Task FetchImages(BaseItem item, IJsonSerializer jsonSerializer, + CancellationToken cancellationToken) + { + var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); + var language = item.GetPreferredMetadataLanguage(); + + if (string.IsNullOrEmpty(tmdbId)) + { + return null; + } + + await MovieDbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + + var path = MovieDbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + + if (!string.IsNullOrEmpty(path)) + { + var fileInfo = new FileInfo(path); + + if (fileInfo.Exists) + { + return jsonSerializer.DeserializeFromFile(path).images; + } + } + + return null; + } + + public int Order + { + get + { + // After tvdb and fanart + return 2; + } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = MovieDbProvider.Current.MovieDbResourcePool + }); + } + + public bool HasChanged(IHasMetadata item, DateTime date) + { + return MovieDbSeriesProvider.Current.HasChanged(item, date); + } + } +} diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs new file mode 100644 index 0000000000..0901860f16 --- /dev/null +++ b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs @@ -0,0 +1,442 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; + +namespace MediaBrowser.Providers.TV +{ + public class MovieDbSeriesProvider : IRemoteMetadataProvider, IHasOrder + { + private const string GetTvInfo3 = @"http://api.themoviedb.org/3/tv/{0}?api_key={1}&append_to_response=casts,images,keywords,external_ids"; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + internal static MovieDbSeriesProvider Current { get; private set; } + + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _configurationManager; + private readonly ILogger _logger; + + public MovieDbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger) + { + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _configurationManager = configurationManager; + _logger = logger; + Current = this; + } + + public string Name + { + get { return "TheMovieDb"; } + } + + public async Task> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + var tmdbId = info.GetProviderId(MetadataProviders.Tmdb); + var imdbId = info.GetProviderId(MetadataProviders.Imdb); + var tvdbId = info.GetProviderId(MetadataProviders.Tvdb); + + // Commenting our searching by imdb/tvdb because as of now it's not supported. + // But this is how movies work so most likely this can eventually be enabled. + + if (string.IsNullOrEmpty(tmdbId) /*&& string.IsNullOrEmpty(imdbId) && string.IsNullOrEmpty(tvdbId)*/) + { + tmdbId = await new MovieDbSearch(_logger, _jsonSerializer).FindSeriesId(info, cancellationToken).ConfigureAwait(false); + } + + if (!string.IsNullOrEmpty(tmdbId) /*|| !string.IsNullOrEmpty(imdbId) || !string.IsNullOrEmpty(tvdbId)*/) + { + cancellationToken.ThrowIfCancellationRequested(); + + result.Item = await FetchMovieData(tmdbId, imdbId, tvdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + + result.HasMetadata = result.Item != null; + } + + return result; + } + + private async Task FetchMovieData(string tmdbId, string imdbId, string tvdbId, string language, string preferredCountryCode, CancellationToken cancellationToken) + { + string dataFilePath = null; + RootObject seriesInfo = null; + + // Id could be ImdbId or TmdbId + if (string.IsNullOrEmpty(tmdbId)) + { + if (string.IsNullOrWhiteSpace(imdbId)) + { + seriesInfo = await FetchMainResult(imdbId, language, cancellationToken).ConfigureAwait(false); + } + if (seriesInfo == null) + { + if (string.IsNullOrWhiteSpace(imdbId)) + { + seriesInfo = await FetchMainResult(tvdbId, language, cancellationToken).ConfigureAwait(false); + } + } + + if (seriesInfo == null) + { + return null; + } + + tmdbId = seriesInfo.id.ToString(_usCulture); + + dataFilePath = MovieDbProvider.Current.GetDataFilePath(tmdbId, language); + Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath); + } + + await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + + dataFilePath = dataFilePath ?? GetDataFilePath(tmdbId, language); + seriesInfo = seriesInfo ?? _jsonSerializer.DeserializeFromFile(dataFilePath); + + var item = new Series(); + + ProcessMainInfo(item, preferredCountryCode, seriesInfo); + + return item; + } + + private void ProcessMainInfo(Series series, string countryCode, RootObject seriesInfo) + { + series.Name = seriesInfo.name; + series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.id.ToString(_usCulture)); + + series.VoteCount = seriesInfo.vote_count; + + string voteAvg = seriesInfo.vote_average.ToString(CultureInfo.InvariantCulture); + float rating; + + if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out rating)) + { + series.CommunityRating = rating; + } + + series.Overview = seriesInfo.overview; + + if (seriesInfo.networks != null) + { + series.Studios = seriesInfo.networks.Select(i => i.name).ToList(); + } + + if (seriesInfo.genres != null) + { + series.Genres = seriesInfo.genres.Select(i => i.name).ToList(); + } + + series.HomePageUrl = seriesInfo.homepage; + + series.RunTimeTicks = seriesInfo.episode_run_time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); + + if (string.Equals(seriesInfo.status, "Ended", StringComparison.OrdinalIgnoreCase)) + { + series.Status = SeriesStatus.Ended; + } + else + { + series.Status = SeriesStatus.Continuing; + } + + series.PremiereDate = seriesInfo.first_air_date; + series.EndDate = seriesInfo.last_air_date; + + var ids = seriesInfo.external_ids; + if (ids != null) + { + if (!string.IsNullOrWhiteSpace(ids.imdb_id)) + { + series.SetProviderId(MetadataProviders.Imdb, ids.imdb_id); + } + if (ids.tvrage_id > 0) + { + series.SetProviderId(MetadataProviders.TvRageSeries, ids.tvrage_id.ToString(_usCulture)); + } + if (ids.tvdb_id > 0) + { + series.SetProviderId(MetadataProviders.Tvdb, ids.tvdb_id.ToString(_usCulture)); + } + } + } + + internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId) + { + var dataPath = GetSeriesDataPath(appPaths); + + return Path.Combine(dataPath, tmdbId); + } + + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.DataPath, "tmdb-tv"); + + return dataPath; + } + + internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + var mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + + if (mainResult == null) return; + + var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); + + Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + + _jsonSerializer.SerializeToFile(mainResult, dataFilePath); + } + + internal async Task FetchMainResult(string id, string language, CancellationToken cancellationToken) + { + var url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey); + + // Get images in english and with no language + url += "&include_image_language=en,null"; + + if (!string.IsNullOrEmpty(language)) + { + // If preferred language isn't english, get those images too + if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + { + url += string.Format(",{0}", language); + } + + url += string.Format("&language={0}", language); + } + + RootObject mainResult; + + cancellationToken.ThrowIfCancellationRequested(); + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + mainResult = _jsonSerializer.DeserializeFromStream(json); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (mainResult != null && string.IsNullOrEmpty(mainResult.overview)) + { + if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + { + _logger.Info("Couldn't find meta for language " + language + ". Trying English..."); + + url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&include_image_language=en,null&language=en"; + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + mainResult = _jsonSerializer.DeserializeFromStream(json); + } + + if (String.IsNullOrEmpty(mainResult.overview)) + { + _logger.Error("Unable to find information for (id:" + id + ")"); + return null; + } + } + } + return mainResult; + } + + private readonly Task _cachedTask = Task.FromResult(true); + internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException("language"); + } + + var path = GetDataFilePath(tmdbId, language); + + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + // If it's recent or automatic updates are enabled, don't re-download + if ((_configurationManager.Configuration.EnableTmdbUpdates) || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7) + { + return _cachedTask; + } + } + + return DownloadSeriesInfo(tmdbId, language, cancellationToken); + } + + internal string GetDataFilePath(string tmdbId, string preferredLanguage) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + if (string.IsNullOrEmpty(preferredLanguage)) + { + throw new ArgumentNullException("preferredLanguage"); + } + + var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + + var filename = string.Format("series-{0}.json", + preferredLanguage ?? string.Empty); + + return Path.Combine(path, filename); + } + + public bool HasChanged(IHasMetadata item, DateTime date) + { + if (!_configurationManager.Configuration.EnableTmdbUpdates) + { + return false; + } + + var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); + + if (!String.IsNullOrEmpty(tmdbId)) + { + // Process images + var dataFilePath = GetDataFilePath(tmdbId, item.GetPreferredMetadataLanguage()); + + var fileInfo = new FileInfo(dataFilePath); + + return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; + } + + return false; + } + + public class CreatedBy + { + public int id { get; set; } + public string name { get; set; } + public string profile_path { get; set; } + } + + public class Genre + { + public int id { get; set; } + public string name { get; set; } + } + + public class Network + { + public int id { get; set; } + public string name { get; set; } + } + + public class Season + { + public string air_date { get; set; } + public string poster_path { get; set; } + public int season_number { get; set; } + } + + public class Backdrop + { + public double aspect_ratio { get; set; } + public string file_path { get; set; } + public int height { get; set; } + public string iso_639_1 { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public int width { get; set; } + } + + public class Poster + { + public double aspect_ratio { get; set; } + public string file_path { get; set; } + public int height { get; set; } + public string iso_639_1 { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public int width { get; set; } + } + + public class Images + { + public List backdrops { get; set; } + public List posters { get; set; } + } + + public class ExternalIds + { + public string imdb_id { get; set; } + public string freebase_id { get; set; } + public string freebase_mid { get; set; } + public int tvdb_id { get; set; } + public int tvrage_id { get; set; } + } + + public class RootObject + { + public string backdrop_path { get; set; } + public List created_by { get; set; } + public List episode_run_time { get; set; } + public DateTime first_air_date { get; set; } + public List genres { get; set; } + public string homepage { get; set; } + public int id { get; set; } + public bool in_production { get; set; } + public List languages { get; set; } + public DateTime last_air_date { get; set; } + public string name { get; set; } + public List networks { get; set; } + public int number_of_episodes { get; set; } + public int number_of_seasons { get; set; } + public string original_name { get; set; } + public List origin_country { get; set; } + public string overview { get; set; } + public string popularity { get; set; } + public string poster_path { get; set; } + public List seasons { get; set; } + public string status { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public Images images { get; set; } + public ExternalIds external_ids { get; set; } + } + + public int Order + { + get + { + // After Tvdb + return 2; + } + } + } +}