New: Option to opt out of TBA episode title import delays

Closes #3086
pull/3105/head
Mark McDowall 6 years ago
parent e70d92f670
commit 9b617af713

@ -13,6 +13,12 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import RootFoldersConnector from 'RootFolder/RootFoldersConnector'; import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
import NamingConnector from './Naming/NamingConnector'; 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 = [ const rescanAfterRefreshOptions = [
{ key: 'always', value: 'Always' }, { key: 'always', value: 'Always' },
{ key: 'afterManual', value: 'After Manual Refresh' }, { key: 'afterManual', value: 'After Manual Refresh' },
@ -116,6 +122,23 @@ class MediaManagement extends Component {
<FieldSet <FieldSet
legend="Importing" legend="Importing"
> >
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.SMALL}
>
<FormLabel>Episode Title Required</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="episodeTitleRequired"
helpText="Prevent importing for up to 24 hours if the episode title is in the naming format and the episode title is TBA"
values={episodeTitleRequiredOptions}
onChange={onInputChange}
{...settings.episodeTitleRequired}
/>
</FormGroup>
{ {
isMono && isMono &&
<FormGroup <FormGroup

@ -2,7 +2,10 @@ using System;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -81,5 +84,66 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
} }
[Test]
public void should_accept_when_episode_title_is_never_required()
{
Mocker.GetMock<IConfigService>()
.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<IConfigService>()
.Setup(s => s.EpisodeTitleRequired)
.Returns(EpisodeTitleRequiredType.BulkSeasonReleases);
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
.Returns(Builder<Episode>.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<IConfigService>()
.Setup(s => s.EpisodeTitleRequired)
.Returns(EpisodeTitleRequiredType.BulkSeasonReleases);
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
.Returns(Builder<Episode>.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<IConfigService>()
.Setup(s => s.EpisodeTitleRequired)
.Returns(EpisodeTitleRequiredType.BulkSeasonReleases);
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
.Returns(Builder<Episode>.CreateListOfSize(5)
.All()
.With(e => e.AirDateUtc = _localEpisode.Episodes.First().AirDateUtc)
.BuildList());
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
} }
} }

@ -8,6 +8,7 @@ using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Security; using NzbDrone.Core.Security;
namespace NzbDrone.Core.Configuration namespace NzbDrone.Core.Configuration
@ -223,6 +224,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("RescanAfterRefresh", value); } set { SetValue("RescanAfterRefresh", value); }
} }
public EpisodeTitleRequiredType EpisodeTitleRequired
{
get { return GetValueEnum("EpisodeTitleRequired", EpisodeTitleRequiredType.Always); }
set { SetValue("EpisodeTitleRequired", value); }
}
public bool SetPermissionsLinux public bool SetPermissionsLinux
{ {
get { return GetValueBoolean("SetPermissionsLinux", false); } get { return GetValueBoolean("SetPermissionsLinux", false); }

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Security; using NzbDrone.Core.Security;
namespace NzbDrone.Core.Configuration namespace NzbDrone.Core.Configuration
@ -35,6 +36,8 @@ namespace NzbDrone.Core.Configuration
bool ImportExtraFiles { get; set; } bool ImportExtraFiles { get; set; }
string ExtraFileExtensions { get; set; } string ExtraFileExtensions { get; set; }
RescanAfterRefreshType RescanAfterRefresh { get; set; } RescanAfterRefreshType RescanAfterRefresh { get; set; }
EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
//Permissions (Media Management) //Permissions (Media Management)
bool SetPermissionsLinux { get; set; } bool SetPermissionsLinux { get; set; }

@ -0,0 +1,9 @@
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public enum EpisodeTitleRequiredType
{
Always = 0,
BulkSeasonReleases = 1,
Never = 2
}
}

@ -1,32 +1,69 @@
using System; using System;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{ {
public class EpisodeTitleSpecification : IImportDecisionEngineSpecification public class EpisodeTitleSpecification : IImportDecisionEngineSpecification
{ {
private readonly IConfigService _configService;
private readonly IBuildFileNames _buildFileNames; private readonly IBuildFileNames _buildFileNames;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger; private readonly Logger _logger;
public EpisodeTitleSpecification(IBuildFileNames buildFileNames, Logger logger) public EpisodeTitleSpecification(IConfigService configService,
IBuildFileNames buildFileNames,
IEpisodeService episodeService,
Logger logger)
{ {
_configService = configService;
_buildFileNames = buildFileNames; _buildFileNames = buildFileNames;
_episodeService = episodeService;
_logger = logger; _logger = logger;
} }
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) 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)) if (!_buildFileNames.RequiresEpisodeTitle(localEpisode.Series, localEpisode.Episodes))
{ {
_logger.Debug("File name format does not require episode title, skipping check"); _logger.Debug("File name format does not require episode title, skipping check");
return Decision.Accept(); 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 airDateUtc = episode.AirDateUtc;
var title = episode.Title; var title = episode.Title;

@ -783,6 +783,7 @@
<Compile Include="Languages\LanguageComparer.cs" /> <Compile Include="Languages\LanguageComparer.cs" />
<Compile Include="Languages\LanguagesBelowCutoff.cs" /> <Compile Include="Languages\LanguagesBelowCutoff.cs" />
<Compile Include="MediaFiles\EpisodeImport\Aggregation\Aggregators\AggregateLanguage.cs" /> <Compile Include="MediaFiles\EpisodeImport\Aggregation\Aggregators\AggregateLanguage.cs" />
<Compile Include="MediaFiles\EpisodeImport\EpisodeTitleRequiredType.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\AbsoluteEpisodeNumberSpecification.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\AbsoluteEpisodeNumberSpecification.cs" />
<Compile Include="Notifications\Discord\Discord.cs" /> <Compile Include="Notifications\Discord\Discord.cs" />
<Compile Include="Notifications\Discord\DiscordColors.cs" /> <Compile Include="Notifications\Discord\DiscordColors.cs" />

@ -1,5 +1,6 @@
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using Sonarr.Http.REST; using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
@ -20,6 +21,7 @@ namespace Sonarr.Api.V3.Config
public string ChownUser { get; set; } public string ChownUser { get; set; }
public string ChownGroup { get; set; } public string ChownGroup { get; set; }
public EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
public bool SkipFreeSpaceCheckWhenImporting { get; set; } public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; } public bool CopyUsingHardlinks { get; set; }
public bool ImportExtraFiles { get; set; } public bool ImportExtraFiles { get; set; }
@ -47,6 +49,7 @@ namespace Sonarr.Api.V3.Config
ChownUser = model.ChownUser, ChownUser = model.ChownUser,
ChownGroup = model.ChownGroup, ChownGroup = model.ChownGroup,
EpisodeTitleRequired = model.EpisodeTitleRequired,
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting, SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
CopyUsingHardlinks = model.CopyUsingHardlinks, CopyUsingHardlinks = model.CopyUsingHardlinks,
ImportExtraFiles = model.ImportExtraFiles, ImportExtraFiles = model.ImportExtraFiles,

Loading…
Cancel
Save