From 9b617af713534c54ef9d582db6dd0316ed311eb9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 1 May 2019 20:36:09 -0700 Subject: [PATCH] New: Option to opt out of TBA episode title import delays Closes #3086 --- .../MediaManagement/MediaManagement.js | 23 +++++++ .../EpisodeTitleSpecificationFixture.cs | 64 +++++++++++++++++++ .../Configuration/ConfigService.cs | 8 +++ .../Configuration/IConfigService.cs | 3 + .../EpisodeImport/EpisodeTitleRequiredType.cs | 9 +++ .../EpisodeTitleSpecification.cs | 41 +++++++++++- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Config/MediaManagementConfigResource.cs | 3 + 8 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/EpisodeTitleRequiredType.cs diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index 0c0da9c0e..e33991ddf 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -13,6 +13,12 @@ import FormInputGroup from 'Components/Form/FormInputGroup'; import RootFoldersConnector from 'RootFolder/RootFoldersConnector'; import NamingConnector from './Naming/NamingConnector'; +const episodeTitleRequiredOptions = [ + { key: 'always', value: 'Always' }, + { key: 'bulkSeasonReleases', value: 'Only for Bulk Season Releases' }, + { key: 'never', value: 'Never' } +]; + const rescanAfterRefreshOptions = [ { key: 'always', value: 'Always' }, { key: 'afterManual', value: 'After Manual Refresh' }, @@ -116,6 +122,23 @@ class MediaManagement extends Component {
+ + Episode Title Required + + + + { isMono && () + .Setup(s => s.EpisodeTitleRequired) + .Returns(EpisodeTitleRequiredType.Never); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_accept_if_episode_title_is_required_for_bulk_season_releases_and_not_bulk_season() + { + Mocker.GetMock() + .Setup(s => s.EpisodeTitleRequired) + .Returns(EpisodeTitleRequiredType.BulkSeasonReleases); + + Mocker.GetMock() + .Setup(s => s.GetEpisodesBySeason(It.IsAny(), It.IsAny())) + .Returns(Builder.CreateListOfSize(5).BuildList()); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_accept_if_episode_title_is_required_for_bulk_season_releases() + { + Mocker.GetMock() + .Setup(s => s.EpisodeTitleRequired) + .Returns(EpisodeTitleRequiredType.BulkSeasonReleases); + + Mocker.GetMock() + .Setup(s => s.GetEpisodesBySeason(It.IsAny(), It.IsAny())) + .Returns(Builder.CreateListOfSize(5) + .All() + .With(e => e.AirDateUtc == _localEpisode.Episodes.First().AirDateUtc) + .BuildList()); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_reject_if_episode_title_is_required_for_bulk_season_releases_and_it_is_mising() + { + _localEpisode.Episodes.First().Title = "TBA"; + + Mocker.GetMock() + .Setup(s => s.EpisodeTitleRequired) + .Returns(EpisodeTitleRequiredType.BulkSeasonReleases); + + + Mocker.GetMock() + .Setup(s => s.GetEpisodesBySeason(It.IsAny(), It.IsAny())) + .Returns(Builder.CreateListOfSize(5) + .All() + .With(e => e.AirDateUtc = _localEpisode.Episodes.First().AirDateUtc) + .BuildList()); + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 3568ec7b9..beabaa284 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Security; namespace NzbDrone.Core.Configuration @@ -223,6 +224,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("RescanAfterRefresh", value); } } + public EpisodeTitleRequiredType EpisodeTitleRequired + { + get { return GetValueEnum("EpisodeTitleRequired", EpisodeTitleRequiredType.Always); } + + set { SetValue("EpisodeTitleRequired", value); } + } + public bool SetPermissionsLinux { get { return GetValueBoolean("SetPermissionsLinux", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 6cf46831d..80d18b2f8 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Security; namespace NzbDrone.Core.Configuration @@ -35,6 +36,8 @@ namespace NzbDrone.Core.Configuration bool ImportExtraFiles { get; set; } string ExtraFileExtensions { get; set; } RescanAfterRefreshType RescanAfterRefresh { get; set; } + EpisodeTitleRequiredType EpisodeTitleRequired { get; set; } + //Permissions (Media Management) bool SetPermissionsLinux { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/EpisodeTitleRequiredType.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/EpisodeTitleRequiredType.cs new file mode 100644 index 000000000..29e5c6355 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/EpisodeTitleRequiredType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.MediaFiles.EpisodeImport +{ + public enum EpisodeTitleRequiredType + { + Always = 0, + BulkSeasonReleases = 1, + Never = 2 + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs index 2ec04f4fd..72e463bb7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/EpisodeTitleSpecification.cs @@ -1,32 +1,69 @@ using System; +using System.Linq; using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { public class EpisodeTitleSpecification : IImportDecisionEngineSpecification { + private readonly IConfigService _configService; private readonly IBuildFileNames _buildFileNames; + private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public EpisodeTitleSpecification(IBuildFileNames buildFileNames, Logger logger) + public EpisodeTitleSpecification(IConfigService configService, + IBuildFileNames buildFileNames, + IEpisodeService episodeService, + Logger logger) { + _configService = configService; _buildFileNames = buildFileNames; + _episodeService = episodeService; _logger = logger; } + public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { + var episodeTitleRequired = _configService.EpisodeTitleRequired; + + if (episodeTitleRequired == EpisodeTitleRequiredType.Never) + { + _logger.Debug("Episode titles are never required, skipping check"); + return Decision.Accept(); + } + 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 episodes = localEpisode.Episodes; + var firstEpisode = episodes.First(); + var episodesInSeason = _episodeService.GetEpisodesBySeason(firstEpisode.SeriesId, firstEpisode.EpisodeNumber); + var allEpisodesOnTheSameDay = firstEpisode.AirDateUtc.HasValue && episodes.All(e => + e.AirDateUtc.HasValue && + e.AirDateUtc.Value == firstEpisode.AirDateUtc.Value); + + if (episodeTitleRequired == EpisodeTitleRequiredType.BulkSeasonReleases && + allEpisodesOnTheSameDay && + episodesInSeason.Count(e => e.AirDateUtc.HasValue && + e.AirDateUtc.Value == firstEpisode.AirDateUtc.Value + ) < 4 + ) + { + _logger.Debug("Episode title only required for bulk season releases"); + return Decision.Accept(); + } + + foreach (var episode in episodes) { var airDateUtc = episode.AirDateUtc; var title = episode.Title; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index d1cc5e1a2..5c3ea3dc0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -783,6 +783,7 @@ + diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs index 048342923..e47578501 100644 --- a/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs @@ -1,5 +1,6 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; using Sonarr.Http.REST; namespace Sonarr.Api.V3.Config @@ -20,6 +21,7 @@ namespace Sonarr.Api.V3.Config public string ChownUser { get; set; } public string ChownGroup { get; set; } + public EpisodeTitleRequiredType EpisodeTitleRequired { get; set; } public bool SkipFreeSpaceCheckWhenImporting { get; set; } public bool CopyUsingHardlinks { get; set; } public bool ImportExtraFiles { get; set; } @@ -47,6 +49,7 @@ namespace Sonarr.Api.V3.Config ChownUser = model.ChownUser, ChownGroup = model.ChownGroup, + EpisodeTitleRequired = model.EpisodeTitleRequired, SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting, CopyUsingHardlinks = model.CopyUsingHardlinks, ImportExtraFiles = model.ImportExtraFiles,