From 6d046a8df8c461dc4c463a3f7bc690582d74b067 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 4 Jul 2015 20:48:26 +0200 Subject: [PATCH] Fixed: Extrapolate scene numbering but won't auto import. --- src/NzbDrone.Api/Episodes/EpisodeResource.cs | 1 + .../SceneNumbering/XemServiceFixture.cs | 119 ++++++++++++++++++ .../DataAugmentation/Xem/XemService.cs | 70 +++++++++++ .../092_add_unverifiedscenenumbering.cs | 14 +++ .../UnverifiedSceneNumberingSpecification.cs | 27 ++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + src/NzbDrone.Core/Tv/Episode.cs | 1 + src/UI/Series/Details/EpisodeWarningCell.js | 10 +- 8 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs diff --git a/src/NzbDrone.Api/Episodes/EpisodeResource.cs b/src/NzbDrone.Api/Episodes/EpisodeResource.cs index 7b419130b..e294989a4 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeResource.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeResource.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Api.Episodes public Nullable SceneAbsoluteEpisodeNumber { get; set; } public Nullable SceneEpisodeNumber { get; set; } public Nullable SceneSeasonNumber { get; set; } + public Boolean UnverifiedSceneNumbering { get; set; } public DateTime? EndTime { get; set; } public DateTime? GrabDate { get; set; } public String SeriesTitle { get; set; } diff --git a/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs index 156fba1ab..fca9cfaed 100644 --- a/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs +++ b/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs @@ -48,6 +48,8 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 3 }); _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 4 }); _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 5 }); + _episodes.Add(new Episode { SeasonNumber = 3, EpisodeNumber = 1 }); + _episodes.Add(new Episode { SeasonNumber = 3, EpisodeNumber = 2 }); Mocker.GetMock() .Setup(v => v.GetEpisodeBySeries(It.IsAny())) @@ -143,5 +145,122 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering Mocker.GetMock() .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never()); } + + [Test] + public void should_flag_unknown_future_episodes_if_existing_season_is_mapped() + { + GivenTvdbMappings(); + _theXemTvdbMappings.RemoveAll(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); + + episode.UnverifiedSceneNumbering.Should().BeTrue(); + } + + [Test] + public void should_flag_unknown_future_season_if_future_season_is_shifted() + { + GivenTvdbMappings(); + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 1); + + episode.UnverifiedSceneNumbering.Should().BeTrue(); + } + + [Test] + public void should_not_flag_unknown_future_season_if_future_season_is_not_shifted() + { + GivenTvdbMappings(); + _theXemTvdbMappings.RemoveAll(v => v.Scene.Season == 3); + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 1); + + episode.UnverifiedSceneNumbering.Should().BeFalse(); + } + + [Test] + public void should_not_extrapolate_season_with_specials() + { + GivenTvdbMappings(); + var specialMapping = _theXemTvdbMappings.First(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); + specialMapping.Tvdb.Season = 0; + specialMapping.Tvdb.Episode = 1; + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); + + episode.UnverifiedSceneNumbering.Should().BeTrue(); + episode.SceneSeasonNumber.Should().NotHaveValue(); + episode.SceneEpisodeNumber.Should().NotHaveValue(); + } + + [Test] + public void should_extrapolate_season_with_future_episodes() + { + GivenTvdbMappings(); + _theXemTvdbMappings.RemoveAll(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); + + episode.UnverifiedSceneNumbering.Should().BeTrue(); + episode.SceneSeasonNumber.Should().Be(3); + episode.SceneEpisodeNumber.Should().Be(2); + } + + [Test] + public void should_extrapolate_season_with_shifted_episodes() + { + GivenTvdbMappings(); + _theXemTvdbMappings.RemoveAll(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); + var dualMapping = _theXemTvdbMappings.First(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 4); + dualMapping.Scene.Season = 2; + dualMapping.Scene.Episode = 3; + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); + + episode.UnverifiedSceneNumbering.Should().BeTrue(); + episode.SceneSeasonNumber.Should().Be(2); + episode.SceneEpisodeNumber.Should().Be(4); + } + + [Test] + public void should_extrapolate_shifted_future_seasons() + { + GivenTvdbMappings(); + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 2); + + episode.UnverifiedSceneNumbering.Should().BeTrue(); + episode.SceneSeasonNumber.Should().Be(4); + episode.SceneEpisodeNumber.Should().Be(2); + } + + [Test] + public void should_not_extrapolate_matching_future_seasons() + { + GivenTvdbMappings(); + _theXemTvdbMappings.RemoveAll(v => v.Scene.Season != 1); + + Subject.Handle(new SeriesUpdatedEvent(_series)); + + var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 2); + + episode.UnverifiedSceneNumbering.Should().BeFalse(); + episode.SceneSeasonNumber.Should().NotHaveValue(); + episode.SceneEpisodeNumber.Should().NotHaveValue(); + } } } diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs index 646b904cb..23a0370b2 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs @@ -52,6 +52,7 @@ namespace NzbDrone.Core.DataAugmentation.Xem episode.SceneAbsoluteEpisodeNumber = null; episode.SceneSeasonNumber = null; episode.SceneEpisodeNumber = null; + episode.UnverifiedSceneNumbering = false; } foreach (var mapping in mappings) @@ -71,6 +72,11 @@ namespace NzbDrone.Core.DataAugmentation.Xem episode.SceneEpisodeNumber = mapping.Scene.Episode; } + if (episodes.Any(v => v.SceneEpisodeNumber.HasValue && v.SceneSeasonNumber != 0)) + { + ExtrapolateMappings(series, episodes, mappings); + } + _episodeService.UpdateEpisodes(episodes); series.UseSceneNumbering = mappings.Any(); _seriesService.UpdateSeries(series); @@ -83,6 +89,70 @@ namespace NzbDrone.Core.DataAugmentation.Xem } } + private void ExtrapolateMappings(Series series, List episodes, List mappings) + { + var mappedEpisodes = episodes.Where(v => v.SeasonNumber != 0 && v.SceneEpisodeNumber.HasValue).ToList(); + var mappedSeasons = new HashSet(mappedEpisodes.Select(v => v.SeasonNumber).Distinct()); + var lastSceneSeason = mappings.Select(v => v.Scene.Season).Max(); + var lastTvdbSeason = mappings.Select(v => v.Tvdb.Season).Max(); + + // Mark all episodes not on the xem as unverified. + foreach (var episode in episodes) + { + if (episode.SeasonNumber == 0) continue; + if (episode.SceneEpisodeNumber.HasValue) continue; + + if (mappedSeasons.Contains(episode.SeasonNumber)) + { + episode.UnverifiedSceneNumbering = true; + } + + if (lastSceneSeason != lastTvdbSeason && episode.SeasonNumber > lastTvdbSeason) + { + episode.UnverifiedSceneNumbering = true; + } + } + + foreach (var episode in episodes) + { + if (episode.SeasonNumber == 0) continue; + if (episode.SceneEpisodeNumber.HasValue) continue; + if (episode.SeasonNumber < lastTvdbSeason) continue; + if (!episode.UnverifiedSceneNumbering) continue; + + var seasonMappings = mappings.Where(v => v.Tvdb.Season == episode.SeasonNumber).ToList(); + if (seasonMappings.Any(v => v.Tvdb.Episode >= episode.EpisodeNumber)) + { + continue; + } + + if (seasonMappings.Any()) + { + var lastEpisodeMapping = seasonMappings.OrderBy(v => v.Tvdb.Episode).Last(); + var lastSceneSeasonMapping = mappings.Where(v => v.Scene.Season == lastEpisodeMapping.Scene.Season).OrderBy(v => v.Scene.Episode).Last(); + + if (lastSceneSeasonMapping.Tvdb.Season == 0) + { + continue; + } + + var offset = episode.EpisodeNumber - lastEpisodeMapping.Tvdb.Episode; + + episode.SceneSeasonNumber = lastEpisodeMapping.Scene.Season; + episode.SceneEpisodeNumber = lastEpisodeMapping.Scene.Episode + offset; + episode.SceneAbsoluteEpisodeNumber = lastEpisodeMapping.Scene.Absolute + offset; + } + else if (lastTvdbSeason != lastSceneSeason) + { + var offset = episode.SeasonNumber - lastTvdbSeason; + + episode.SceneSeasonNumber = lastSceneSeason + offset; + episode.SceneEpisodeNumber = episode.EpisodeNumber; + // TODO: SceneAbsoluteEpisodeNumber. + } + } + } + private void RefreshCache() { var ids = _xemProxy.GetXemSeriesIds(); diff --git a/src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs b/src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs new file mode 100644 index 000000000..5366b0fad --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(92)] + public class add_unverifiedscenenumbering : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Episodes").AddColumn("UnverifiedSceneNumbering").AsBoolean().WithDefaultValue(false); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs new file mode 100644 index 000000000..0a5fa8018 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs @@ -0,0 +1,27 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Parser.Model; +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class UnverifiedSceneNumberingSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + + public UnverifiedSceneNumberingSpecification(Logger logger) + { + _logger = logger; + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode) + { + if (localEpisode.Episodes.Any(v => v.UnverifiedSceneNumbering)) + { + _logger.Debug("This file uses unverified scene numbers, will not auto-import until numbering is confirmed on TheXEM. Skipping {0}", localEpisode.Path); + return Decision.Reject("This show has individual episode mappings on TheXEM but the mapping for this episode has not been confirmed yet by their administrators. TheXEM needs manual input."); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 67a841331..a37a21353 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -264,6 +264,7 @@ + @@ -621,6 +622,7 @@ + diff --git a/src/NzbDrone.Core/Tv/Episode.cs b/src/NzbDrone.Core/Tv/Episode.cs index e0032f62f..ea4658249 100644 --- a/src/NzbDrone.Core/Tv/Episode.cs +++ b/src/NzbDrone.Core/Tv/Episode.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Tv public Nullable SceneAbsoluteEpisodeNumber { get; set; } public Nullable SceneSeasonNumber { get; set; } public Nullable SceneEpisodeNumber { get; set; } + public bool UnverifiedSceneNumbering { get; set; } public Ratings Ratings { get; set; } public List Images { get; set; } diff --git a/src/UI/Series/Details/EpisodeWarningCell.js b/src/UI/Series/Details/EpisodeWarningCell.js index 3a414224d..c9befe7a1 100644 --- a/src/UI/Series/Details/EpisodeWarningCell.js +++ b/src/UI/Series/Details/EpisodeWarningCell.js @@ -7,10 +7,12 @@ module.exports = NzbDroneCell.extend({ render : function() { this.$el.empty(); - if (SeriesCollection.get(this.model.get('seriesId')).get('seriesType') === 'anime') { - if (this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { - this.$el.html(''); - } + if (this.model.get('unverifiedSceneNumbering')) { + this.$el.html(''); + } + + else if (SeriesCollection.get(this.model.get('seriesId')).get('seriesType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { + this.$el.html(''); } this.delegateEvents();