Browse Source

added series-level movie db support

Luke Pulverenti 11 years ago
parent
commit
41b9ce56ef

+ 4 - 4
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
         /// <returns>Task{System.Object}.</returns>
         private async Task<object> 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))

+ 2 - 1
MediaBrowser.Model/Entities/MetadataProviders.cs

@@ -37,6 +37,7 @@ namespace MediaBrowser.Model.Entities
         MusicBrainzReleaseGroup,
         Zap2It,
         NesBox,
-        NesBoxRom
+        NesBoxRom,
+        TvRageSeries
     }
 }

+ 2 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -171,6 +171,8 @@
     <Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" />
     <Compile Include="TV\FanartSeasonProvider.cs" />
     <Compile Include="TV\FanartSeriesProvider.cs" />
+    <Compile Include="TV\MovieDbSeriesImageProvider.cs" />
+    <Compile Include="TV\MovieDbSeriesProvider.cs" />
     <Compile Include="TV\SeriesMetadataService.cs" />
     <Compile Include="TV\TvdbEpisodeImageProvider.cs" />
     <Compile Include="People\TvdbPersonImageProvider.cs" />

+ 0 - 2
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
 {

+ 0 - 1
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;

+ 5 - 0
MediaBrowser.Providers/Movies/MovieDbSearch.cs

@@ -29,6 +29,11 @@ namespace MediaBrowser.Providers.Movies
             _json = json;
         }
 
+        public Task<string> FindSeriesId(ItemLookupInfo idInfo, CancellationToken cancellationToken)
+        {
+            return FindId(idInfo, "tv", cancellationToken);
+        }
+
         public Task<string> FindMovieId(ItemLookupInfo idInfo, CancellationToken cancellationToken)
         {
             return FindId(idInfo, "movie", cancellationToken);

+ 211 - 0
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<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Backdrop
+            };
+        }
+
+        public async Task<IEnumerable<RemoteImageInfo>> 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<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        {
+            var list = new List<RemoteImageInfo>();
+
+            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();
+        }
+
+        /// <summary>
+        /// Gets the posters.
+        /// </summary>
+        /// <param name="images">The images.</param>
+        private IEnumerable<MovieDbSeriesProvider.Poster> GetPosters(MovieDbSeriesProvider.Images images)
+        {
+            return images.posters ?? new List<MovieDbSeriesProvider.Poster>();
+        }
+
+        /// <summary>
+        /// Gets the backdrops.
+        /// </summary>
+        /// <param name="images">The images.</param>
+        private IEnumerable<MovieDbSeriesProvider.Backdrop> GetBackdrops(MovieDbSeriesProvider.Images images)
+        {
+            var eligibleBackdrops = images.backdrops == null ? new List<MovieDbSeriesProvider.Backdrop>() :
+                images.backdrops
+                .ToList();
+
+            return eligibleBackdrops.OrderByDescending(i => i.vote_average)
+                .ThenByDescending(i => i.vote_count);
+        }
+
+        /// <summary>
+        /// Fetches the images.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="jsonSerializer">The json serializer.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{MovieImages}.</returns>
+        private async Task<MovieDbSeriesProvider.Images> 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<MovieDbSeriesProvider.RootObject>(path).images;
+                }
+            }
+
+            return null;
+        }
+
+        public int Order
+        {
+            get
+            {
+                // After tvdb and fanart
+                return 2;
+            }
+        }
+
+        public Task<HttpResponseInfo> 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);
+        }
+    }
+}

+ 442 - 0
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<Series,SeriesInfo>, 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<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
+        {
+            var result = new MetadataResult<Series>();
+
+            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<Series> 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<RootObject>(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<RootObject> 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<RootObject>(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<RootObject>(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<Backdrop> backdrops { get; set; }
+            public List<Poster> 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<CreatedBy> created_by { get; set; }
+            public List<int> episode_run_time { get; set; }
+            public DateTime first_air_date { get; set; }
+            public List<Genre> genres { get; set; }
+            public string homepage { get; set; }
+            public int id { get; set; }
+            public bool in_production { get; set; }
+            public List<string> languages { get; set; }
+            public DateTime last_air_date { get; set; }
+            public string name { get; set; }
+            public List<Network> 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<string> origin_country { get; set; }
+            public string overview { get; set; }
+            public string popularity { get; set; }
+            public string poster_path { get; set; }
+            public List<Season> 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;
+            }
+        }
+    }
+}