diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs new file mode 100644 index 000000000..16a66a4f4 --- /dev/null +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource.SkyHook; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; +using NzbDrone.Test.Common.Categories; + +namespace NzbDrone.Core.Test.MetadataSource.SkyHook +{ + [TestFixture] + [IntegrationTest] + public class SkyHookProxyFixture : CoreTest + { + [SetUp] + public void Setup() + { + UseRealHttp(); + } + + [TestCase(75978, "Family Guy")] + [TestCase(83462, "Castle (2009)")] + [TestCase(266189, "The Blacklist")] + public void should_be_able_to_get_series_detail(int tvdbId, string title) + { + var details = Subject.GetSeriesInfo(tvdbId); + + ValidateSeries(details.Item1); + ValidateEpisodes(details.Item2); + + details.Item1.Title.Should().Be(title); + } + + [Test] + public void getting_details_of_invalid_series() + { + Assert.Throws(() => Subject.GetSeriesInfo(Int32.MaxValue)); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_not_have_period_at_start_of_title_slug() + { + var details = Subject.GetSeriesInfo(79099); + + details.Item1.TitleSlug.Should().Be("dothack"); + } + + private void ValidateSeries(Series series) + { + series.Should().NotBeNull(); + series.Title.Should().NotBeNullOrWhiteSpace(); + series.CleanTitle.Should().Be(Parser.Parser.CleanSeriesTitle(series.Title)); + series.SortTitle.Should().Be(SeriesTitleNormalizer.Normalize(series.Title, series.TvdbId)); + series.Overview.Should().NotBeNullOrWhiteSpace(); + series.AirTime.Should().NotBeNullOrWhiteSpace(); + series.FirstAired.Should().HaveValue(); + series.FirstAired.Value.Kind.Should().Be(DateTimeKind.Utc); + series.Images.Should().NotBeEmpty(); + series.ImdbId.Should().NotBeNullOrWhiteSpace(); + series.Network.Should().NotBeNullOrWhiteSpace(); + series.Runtime.Should().BeGreaterThan(0); + series.TitleSlug.Should().NotBeNullOrWhiteSpace(); + //series.TvRageId.Should().BeGreaterThan(0); + series.TvdbId.Should().BeGreaterThan(0); + } + + private void ValidateEpisodes(List episodes) + { + episodes.Should().NotBeEmpty(); + + var episodeGroup = episodes.GroupBy(e => e.SeasonNumber.ToString("000") + e.EpisodeNumber.ToString("000")); + episodeGroup.Should().OnlyContain(c => c.Count() == 1); + + episodes.Should().Contain(c => c.SeasonNumber > 0); + episodes.Should().Contain(c => !String.IsNullOrWhiteSpace(c.Overview)); + + foreach (var episode in episodes) + { + ValidateEpisode(episode); + + //if atleast one episdoe has title it means parse it working. + episodes.Should().Contain(c => !String.IsNullOrWhiteSpace(c.Title)); + } + } + + private void ValidateEpisode(Episode episode) + { + episode.Should().NotBeNull(); + + //TODO: Is there a better way to validate that episode number or season number is greater than zero? + (episode.EpisodeNumber + episode.SeasonNumber).Should().NotBe(0); + + episode.Should().NotBeNull(); + + if (episode.AirDateUtc.HasValue) + { + episode.AirDateUtc.Value.Kind.Should().Be(DateTimeKind.Utc); + } + + episode.Images.Any(i => i.CoverType == MediaCoverTypes.Screenshot && i.Url.Contains("-940.")) + .Should() + .BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 7fb102c75..046d8238b 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -230,6 +230,7 @@ + diff --git a/src/NzbDrone.Core/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs index 1c3d60f81..dd1191eb0 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser tvShow.Add(new XElement("PremiereDate", series.FirstAired.Value.ToString("yyyy-MM-dd"))); } //tvShow.Add(new XElement("EndDate", series.EndDate.ToString("yyyy-MM-dd"))); - tvShow.Add(new XElement("Rating", (decimal)series.Ratings.Percentage / 10)); + tvShow.Add(new XElement("Rating", series.Ratings.Value)); //tvShow.Add(new XElement("VoteCount", tvShow.Add(new XElement("ProductionYear", series.Year)); //tvShow.Add(new XElement("Website", diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index eaddbcd46..4dd01cdd6 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -171,7 +171,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc var tvShow = new XElement("tvshow"); tvShow.Add(new XElement("title", series.Title)); - tvShow.Add(new XElement("rating", (decimal) series.Ratings.Percentage/10)); + tvShow.Add(new XElement("rating", series.Ratings.Value)); tvShow.Add(new XElement("plot", series.Overview)); tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); @@ -252,7 +252,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } details.Add(new XElement("watched", "false")); - details.Add(new XElement("rating", (decimal)episode.Ratings.Percentage / 10)); + details.Add(new XElement("rating", episode.Ratings.Value)); //Todo: get guest stars, writer and director //details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault())); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ActorResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ActorResource.cs new file mode 100644 index 000000000..180933387 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ActorResource.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class ActorResource + { + public string Name { get; set; } + public string Character { get; set; } + public string Image { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EpisodeResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EpisodeResource.cs new file mode 100644 index 000000000..acaffe418 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EpisodeResource.cs @@ -0,0 +1,17 @@ +using System; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class EpisodeResource + { + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + public int? AbsoluteEpisodeNumber { get; set; } + public string Title { get; set; } + public string AirDate { get; set; } + public DateTime? AirDateUtc { get; set; } + public RatingResource Rating { get; set; } + public string Overview { get; set; } + public string Image { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs new file mode 100644 index 000000000..81a2f578e --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class ImageResource + { + public string CoverType { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs new file mode 100644 index 000000000..d3958378b --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class RatingResource + { + public int Count { get; set; } + public decimal Value { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/SeasonResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/SeasonResource.cs new file mode 100644 index 000000000..55ce6ccf9 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/SeasonResource.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class SeasonResource + { + public SeasonResource() + { + Images = new List(); + } + + public int SeasonNumber { get; set; } + public List Images { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs new file mode 100644 index 000000000..36b1caa5e --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class ShowResource + { + public ShowResource() + { + Actors = new List(); + Genres = new List(); + Images = new List(); + Seasons = new List(); + Episodes = new List(); + } + + public int TvdbId { get; set; } + public string Title { get; set; } + public string Overview { get; set; } + //public string Language { get; set; } + public string Slug { get; set; } + public string FirstAired { get; set; } + public int? TvRageId { get; set; } + + public string Status { get; set; } + public int? Runtime { get; set; } + public TimeOfDayResource TimeOfDay { get; set; } + + public string Network { get; set; } + public string ImdbId { get; set; } + + public List Actors { get; set; } + public List Genres { get; set; } + + public string ContentRating { get; set; } + + public RatingResource Rating { get; set; } + + public List Images { get; set; } + public List Seasons { get; set; } + public List Episodes { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TimeOfDayResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TimeOfDayResource.cs new file mode 100644 index 000000000..242f92a7c --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TimeOfDayResource.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class TimeOfDayResource + { + public int Hours { get; set; } + public int Minutes { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs new file mode 100644 index 000000000..365245265 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource.SkyHook.Resource; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class SkyHookProxy : IProvideSeriesInfo + { + private readonly Logger _logger; + private readonly IHttpClient _httpClient; + private readonly HttpRequestBuilder _requestBuilder; + + public SkyHookProxy(Logger logger, IHttpClient httpClient) + { + _logger = logger; + _httpClient = httpClient; + + _requestBuilder = new HttpRequestBuilder("http://skyhook.sonarr.tv/v1/tvdb/shows/en/"); + } + + public Tuple> GetSeriesInfo(int tvdbSeriesId) + { + var httpRequest = _requestBuilder.Build(tvdbSeriesId.ToString()); + var httpResponse = _httpClient.Get(httpRequest); + var episodes = httpResponse.Resource.Episodes.Select(MapEpisode); + var series = MapSeries(httpResponse.Resource); + + return new Tuple>(series, episodes.ToList()); + } + + private static Series MapSeries(ShowResource show) + { + var series = new Series(); + series.TvdbId = show.TvdbId; + + if (show.TvRageId.HasValue) + { + series.TvRageId = show.TvRageId.Value; + } + + series.ImdbId = show.ImdbId; + series.Title = show.Title; + series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.Title); + series.SortTitle = SeriesTitleNormalizer.Normalize(show.Title, show.TvdbId); + + if (show.FirstAired != null) + { + series.FirstAired = DateTime.Parse(show.FirstAired).ToUniversalTime(); + series.Year = series.FirstAired.Value.Year; + } + + series.Overview = show.Overview; + + if (show.Runtime != null) + { + series.Runtime = show.Runtime.Value; + } + + series.Network = show.Network; + + if (show.TimeOfDay != null) + { + series.AirTime = string.Format("{0:00}:{1:00}", show.TimeOfDay.Hours, show.TimeOfDay.Minutes); + } + + series.TitleSlug = show.Slug; + series.Status = MapSeriesStatus(show.Status); + series.Ratings = MapRatings(show.Rating); + series.Genres = show.Genres; + + if (show.ContentRating.IsNotNullOrWhiteSpace()) + { + series.Certification = show.ContentRating.ToUpper(); + } + + series.Actors = show.Actors.Select(MapActors).ToList(); + series.Seasons = show.Seasons.Select(MapSeason).ToList(); + series.Images = show.Images.Select(MapImage).ToList(); + + return series; + } + + private static Actor MapActors(ActorResource arg) + { + var newActor = new Actor + { + Name = arg.Name, + Character = arg.Character, + + }; + + if (arg.Image != null) + { + newActor.Images = new List + { + new MediaCover.MediaCover(MediaCoverTypes.Headshot, arg.Image) + }; + } + + return newActor; + } + + private static Episode MapEpisode(EpisodeResource oracleEpisode) + { + var episode = new Episode(); + episode.Overview = oracleEpisode.Overview; + episode.SeasonNumber = oracleEpisode.SeasonNumber; + episode.EpisodeNumber = oracleEpisode.EpisodeNumber; + episode.AbsoluteEpisodeNumber = oracleEpisode.AbsoluteEpisodeNumber; + episode.Title = oracleEpisode.Title; + + episode.AirDate = oracleEpisode.AirDate; + episode.AirDateUtc = oracleEpisode.AirDateUtc; + + if (oracleEpisode.Rating != null) + { + episode.Ratings = MapRatings(oracleEpisode.Rating); + } + + //Don't include series fanart images as episode screenshot + if (oracleEpisode.Image != null) + { + episode.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Screenshot, oracleEpisode.Image)); + } + + return episode; + } + + private static Season MapSeason(SeasonResource seasonResource) + { + return new Season + { + SeasonNumber = seasonResource.SeasonNumber, + Images = seasonResource.Images.Select(MapImage).ToList() + }; + } + + private static SeriesStatusType MapSeriesStatus(string status) + { + if (status.Equals("ended", StringComparison.InvariantCultureIgnoreCase)) + { + return SeriesStatusType.Ended; + } + + return SeriesStatusType.Continuing; + } + + private static Ratings MapRatings(RatingResource rating) + { + if (rating == null) + { + return new Ratings(); + } + + return new Ratings + { + Votes = rating.Count, + Value = rating.Value + }; + } + + private static MediaCover.MediaCover MapImage(ImageResource arg) + { + return new MediaCover.MediaCover + { + Url = arg.Url, + CoverType = MapCoverType(arg.CoverType) + }; + } + + private static MediaCoverTypes MapCoverType(string coverType) + { + switch (coverType.ToLower()) + { + case "poster": + return MediaCoverTypes.Poster; + case "banner": + return MediaCoverTypes.Banner; + case "fanart": + return MediaCoverTypes.Fanart; + default: + return MediaCoverTypes.Unknown; + } + } + + } +} diff --git a/src/NzbDrone.Core/MetadataSource/TvDbProxy.cs b/src/NzbDrone.Core/MetadataSource/TvDbProxy.cs index 2c3113040..be12009f4 100644 --- a/src/NzbDrone.Core/MetadataSource/TvDbProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TvDbProxy.cs @@ -14,7 +14,7 @@ using TVDBSharp.Models.Enums; namespace NzbDrone.Core.MetadataSource { - public class TvDbProxy : ISearchForNewSeries, IProvideSeriesInfo + public class TvDbProxy : ISearchForNewSeries { private readonly Logger _logger; private static readonly Regex CollapseSpaceRegex = new Regex(@"\s+", RegexOptions.Compiled); @@ -215,14 +215,14 @@ namespace NzbDrone.Core.MetadataSource return phrase; } - private static Tv.Ratings GetRatings(int ratingCount, double? rating) + private static Tv.Ratings GetRatings(int ratingCount, decimal? rating) { var result = new Tv.Ratings { Votes = ratingCount }; if (rating != null) { - result.Percentage = (int)(rating.Value * 100); + result.Value = rating.Value; } return result; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5d77d8444..e059204dc 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -596,6 +596,14 @@ + + + + + + + + diff --git a/src/NzbDrone.Core/Tv/Ratings.cs b/src/NzbDrone.Core/Tv/Ratings.cs index ffdabf95e..7eff14392 100644 --- a/src/NzbDrone.Core/Tv/Ratings.cs +++ b/src/NzbDrone.Core/Tv/Ratings.cs @@ -5,9 +5,7 @@ namespace NzbDrone.Core.Tv { public class Ratings : IEmbeddedDocument { - public Int32 Percentage { get; set; } - public Int32 Votes { get; set; } - public Int32 Loved { get; set; } - public Int32 Hated { get; set; } + public int Votes { get; set; } + public decimal Value { get; set; } } } diff --git a/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs b/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs index 39f267679..e909db6f3 100644 --- a/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs +++ b/src/NzbDrone.Integration.Test/EpisodeIntegrationTests.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using FluentAssertions; using NUnit.Framework; using NzbDrone.Api.Series; @@ -15,12 +16,12 @@ namespace NzbDrone.Integration.Test [SetUp] public void Setup() { - series = GivenSeriesWithEpisodes(); + series = GivenSeriesWithEpisodes(); } - + private SeriesResource GivenSeriesWithEpisodes() { - var newSeries = Series.Lookup("archer").First(); + var newSeries = Series.Lookup("archer").Single(c => c.TvdbId == 110381); newSeries.ProfileId = 1; newSeries.Path = @"C:\Test\Archer".AsOsAgnostic(); @@ -34,6 +35,7 @@ namespace NzbDrone.Integration.Test return newSeries; } + Console.WriteLine("Waiting for episodes to load."); Thread.Sleep(1000); } } diff --git a/src/TVDBSharp/Models/Builder.cs b/src/TVDBSharp/Models/Builder.cs index dd79c6f85..ecdb69511 100644 --- a/src/TVDBSharp/Models/Builder.cs +++ b/src/TVDBSharp/Models/Builder.cs @@ -105,8 +105,8 @@ namespace TVDBSharp.Models _show.Network = doc.GetSeriesData("Network"); _show.Description = doc.GetSeriesData("Overview"); _show.Rating = string.IsNullOrWhiteSpace(doc.GetSeriesData("Rating")) - ? (double?) null - : Convert.ToDouble(doc.GetSeriesData("Rating"), + ? (decimal?) null + : Convert.ToDecimal(doc.GetSeriesData("Rating"), System.Globalization.CultureInfo.InvariantCulture); _show.RatingCount = string.IsNullOrWhiteSpace(doc.GetSeriesData("RatingCount")) ? 0 @@ -182,8 +182,8 @@ namespace TVDBSharp.Models : Convert.ToInt64(episodeNode.GetXmlData("lastupdated")), Rating = string.IsNullOrWhiteSpace(episodeNode.GetXmlData("Rating")) - ? (double?) null - : Convert.ToDouble(episodeNode.GetXmlData("Rating"), + ? (Decimal?) null + : Convert.ToDecimal(episodeNode.GetXmlData("Rating"), System.Globalization.CultureInfo.InvariantCulture), RatingCount = string.IsNullOrWhiteSpace(episodeNode.GetXmlData("RatingCount")) diff --git a/src/TVDBSharp/Models/Episode.cs b/src/TVDBSharp/Models/Episode.cs index e53d321ae..23e53d64d 100644 --- a/src/TVDBSharp/Models/Episode.cs +++ b/src/TVDBSharp/Models/Episode.cs @@ -66,7 +66,7 @@ namespace TVDBSharp.Models /// /// Average rating as shown on IMDb. /// - public double? Rating { get; set; } + public decimal? Rating { get; set; } /// /// Amount of votes cast. diff --git a/src/TVDBSharp/Models/Show.cs b/src/TVDBSharp/Models/Show.cs index ee1ec4463..5b671bb06 100644 --- a/src/TVDBSharp/Models/Show.cs +++ b/src/TVDBSharp/Models/Show.cs @@ -67,7 +67,7 @@ namespace TVDBSharp.Models /// /// Average rating as shown on IMDb. /// - public double? Rating { get; set; } + public decimal? Rating { get; set; } /// /// Amount of votes cast.