From a023732c1c96141baa0e91ad05f91757ee4f23c7 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 20 Jan 2018 23:56:35 -0800 Subject: [PATCH] New: Delay import of episodes without titles temporarily Closes #2098 --- .../EpisodeTitleSpecificationFixture.cs | 85 +++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 2 + .../RequiresEpisodeTitleFixture.cs | 57 +++++++++++++ .../TvTests/ShouldRefreshSeriesFixture.cs | 2 +- .../EpisodeTitleSpecification.cs | 58 +++++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Organizer/FileNameBuilder.cs | 39 ++++++++- src/NzbDrone.Core/Tv/SeriesService.cs | 5 +- 8 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecificationFixture.cs create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/RequiresEpisodeTitleFixture.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecificationFixture.cs new file mode 100644 index 000000000..58e50fc7c --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecificationFixture.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications +{ + [TestFixture] + public class EpisodeTitleSpecificationFixture : CoreTest + { + private Series _series; + private LocalEpisode _localEpisode; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.SeriesType = SeriesTypes.Standard) + .With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic()) + .Build(); + + var episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.SeasonNumber = 1) + .With(e => e.AirDateUtc = DateTime.UtcNow) + .Build() + .ToList(); + + _localEpisode = new LocalEpisode + { + Path = @"C:\Test\Unsorted\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(), + Episodes = episodes, + Series = _series + }; + + Mocker.GetMock() + .Setup(s => s.RequiresEpisodeTitle(_series, episodes)) + .Returns(true); + } + + [Test] + public void should_reject_when_title_is_null() + { + _localEpisode.Episodes.First().Title = null; + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_reject_when_title_is_TBA() + { + _localEpisode.Episodes.First().Title = "TBA"; + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_accept_when_did_not_air_recently_but_title_is_TBA() + { + _localEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow.AddDays(-7); + _localEpisode.Episodes.First().Title = "TBA"; + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_accept_when_episode_title_is_not_required() + { + _localEpisode.Episodes.First().Title = "TBA"; + + Mocker.GetMock() + .Setup(s => s.RequiresEpisodeTitle(_series, _localEpisode.Episodes)) + .Returns(false); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 0c803ae98..41154a704 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -300,6 +300,7 @@ + @@ -320,6 +321,7 @@ + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/RequiresEpisodeTitleFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/RequiresEpisodeTitleFixture.cs new file mode 100644 index 000000000..3837c29b5 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/RequiresEpisodeTitleFixture.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + public class RequiresEpisodeTitleFixture : CoreTest + { + private Series _series; + private Episode _episode; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .With(s => s.Title = "South Park") + .Build(); + + _episode = Builder.CreateNew() + .With(e => e.Title = "City Sushi") + .With(e => e.SeasonNumber = 15) + .With(e => e.EpisodeNumber = 6) + .With(e => e.AbsoluteEpisodeNumber = 100) + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + } + + [Test] + public void should_return_false_when_episode_title_is_not_part_of_the_pattern() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} S{season:00}E{episode:00}"; + Subject.RequiresEpisodeTitle(_series, new List { _episode }).Should().BeFalse(); + } + + [Test] + public void should_return_true_when_episode_title_is_part_of_the_pattern() + { + Subject.RequiresEpisodeTitle(_series, new List { _episode }).Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs index 6fb44c09a..cbbaf0146 100644 --- a/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.TvTests private void GivenSeriesLastRefreshedRecently() { - _series.LastInfoSync = DateTime.UtcNow.AddHours(-1); + _series.LastInfoSync = DateTime.UtcNow.AddMinutes(-30); } private void GivenRecentlyAired() diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs new file mode 100644 index 000000000..2ec04f4fd --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs @@ -0,0 +1,58 @@ +using System; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class EpisodeTitleSpecification : IImportDecisionEngineSpecification + { + private readonly IBuildFileNames _buildFileNames; + private readonly Logger _logger; + + public EpisodeTitleSpecification(IBuildFileNames buildFileNames, Logger logger) + { + _buildFileNames = buildFileNames; + _logger = logger; + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + if (!_buildFileNames.RequiresEpisodeTitle(localEpisode.Series, localEpisode.Episodes)) + { + _logger.Debug("File name format does not require episode title, skipping check"); + return Decision.Accept(); + } + + foreach (var episode in localEpisode.Episodes) + { + var airDateUtc = episode.AirDateUtc; + var title = episode.Title; + + if (airDateUtc.HasValue && airDateUtc.Value.Before(DateTime.UtcNow.AddDays(-1))) + { + _logger.Debug("Episode aired more than 1 day ago"); + continue; + } + + if (title.IsNullOrWhiteSpace()) + { + _logger.Debug("Episode does not have a title and recently aired"); + + return Decision.Reject("Episode does not have a title and recently aired"); + } + + if (title.Equals("TBA")) + { + _logger.Debug("Episode has a TBA title and recently aired"); + + return Decision.Reject("Episode has a TBA title and recently aired"); + } + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index ed2ff9853..c6851055d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -789,6 +789,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index ff9ae607b..f4737bc37 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Organizer BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + bool RequiresEpisodeTitle(Series series, List episodes); } public class FileNameBuilder : IBuildFileNames @@ -31,6 +32,7 @@ namespace NzbDrone.Core.Organizer private readonly IQualityDefinitionService _qualityDefinitionService; private readonly ICached _episodeFormatCache; private readonly ICached _absoluteEpisodeFormatCache; + private readonly ICached _requiresEpisodeTitleCache; private readonly Logger _logger; private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", @@ -78,6 +80,7 @@ namespace NzbDrone.Core.Organizer _qualityDefinitionService = qualityDefinitionService; _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); + _requiresEpisodeTitleCache = cacheManager.GetCache(GetType(), "requiresEpisodeTitle"); _logger = logger; } @@ -280,6 +283,40 @@ namespace NzbDrone.Core.Organizer return name.Trim(' ', '.'); } + public bool RequiresEpisodeTitle(Series series, List episodes) + { + var namingConfig = _namingConfigService.GetConfig(); + var pattern = namingConfig.StandardEpisodeFormat; + + if (series.SeriesType == SeriesTypes.Daily) + { + pattern = namingConfig.DailyEpisodeFormat; + } + + if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) + { + pattern = namingConfig.AnimeEpisodeFormat; + } + + return _requiresEpisodeTitleCache.Get(pattern, () => + { + var matches = TitleRegex.Matches(pattern); + + foreach (Match match in matches) + { + var token = match.Groups["token"].Value; + + if (FileNameBuilderTokenEqualityComparer.Instance.Equals(token, "{Episode Title}") || + FileNameBuilderTokenEqualityComparer.Instance.Equals(token, "{Episode CleanTitle}")) + { + return true; + } + } + + return false; + }); + } + private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) { tokenHandlers["{Series Title}"] = m => series.Title; diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 375363f24..79166d843 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; -using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser;