Merge pull request #156 from Sonarr/add-series-options
Add Series Options
This commit is contained in:
commit
27d3ecf6b2
|
@ -16,6 +16,7 @@ using NzbDrone.Core.Tv.Events;
|
|||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.Core.DataAugmentation.Scene;
|
||||
using NzbDrone.SignalR;
|
||||
using Omu.ValueInjecter;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
|
@ -109,7 +110,9 @@ namespace NzbDrone.Api.Series
|
|||
|
||||
private int AddSeries(SeriesResource seriesResource)
|
||||
{
|
||||
return GetNewId<Core.Tv.Series>(_seriesService.AddSeries, seriesResource);
|
||||
var series = _seriesService.AddSeries(seriesResource.InjectTo<Core.Tv.Series>());
|
||||
|
||||
return series.Id;
|
||||
}
|
||||
|
||||
private void UpdateSeries(SeriesResource seriesResource)
|
||||
|
|
|
@ -67,6 +67,7 @@ namespace NzbDrone.Api.Series
|
|||
public List<String> Genres { get; set; }
|
||||
public HashSet<Int32> Tags { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public AddSeriesOptions AddOptions { get; set; }
|
||||
|
||||
//Used to support legacy consumers
|
||||
public Int32 QualityProfileId
|
||||
|
|
|
@ -317,6 +317,7 @@
|
|||
<Compile Include="TvTests\MoveSeriesServiceFixture.cs" />
|
||||
<Compile Include="TvTests\RefreshEpisodeServiceFixture.cs" />
|
||||
<Compile Include="TvTests\RefreshSeriesServiceFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesAddedHandlerTests\SetEpisodeMontitoredFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesRepositoryTests\SeriesRepositoryFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesServiceTests\AddSeriesFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" />
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
namespace NzbDrone.Core.Test.TvTests.SeriesAddedHandlerTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SetEpisodeMontitoredFixture : CoreTest<SeriesScannedHandler>
|
||||
{
|
||||
private Series _series;
|
||||
private List<Episode> _episodes;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var seasons = 4;
|
||||
|
||||
_series = Builder<Series>.CreateNew()
|
||||
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(seasons)
|
||||
.All()
|
||||
.With(n => n.Monitored = true)
|
||||
.Build()
|
||||
.ToList())
|
||||
.Build();
|
||||
|
||||
_episodes = Builder<Episode>.CreateListOfSize(seasons)
|
||||
.All()
|
||||
.With(e => e.Monitored = true)
|
||||
.With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-7))
|
||||
//Missing
|
||||
.TheFirst(1)
|
||||
.With(e => e.EpisodeFileId = 0)
|
||||
//Has File
|
||||
.TheNext(1)
|
||||
.With(e => e.EpisodeFileId = 1)
|
||||
//Future
|
||||
.TheNext(1)
|
||||
.With(e => e.EpisodeFileId = 0)
|
||||
.With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(7))
|
||||
//Future/TBA
|
||||
.TheNext(1)
|
||||
.With(e => e.EpisodeFileId = 0)
|
||||
.With(e => e.AirDateUtc = null)
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Setup(s => s.GetEpisodeBySeries(It.IsAny<int>()))
|
||||
.Returns(_episodes);
|
||||
}
|
||||
|
||||
private void WithSeriesAddedEvent(AddSeriesOptions options)
|
||||
{
|
||||
_series.AddOptions = options;
|
||||
}
|
||||
|
||||
private void TriggerSeriesScannedEvent()
|
||||
{
|
||||
Subject.Handle(new SeriesScannedEvent(_series));
|
||||
}
|
||||
|
||||
private void GivenSpecials()
|
||||
{
|
||||
foreach (var episode in _episodes)
|
||||
{
|
||||
episode.SeasonNumber = 0;
|
||||
}
|
||||
|
||||
_series.Seasons = new List<Season>{new Season { Monitored = false, SeasonNumber = 0 }};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_monitor_all_episodes()
|
||||
{
|
||||
WithSeriesAddedEvent(new AddSeriesOptions());
|
||||
TriggerSeriesScannedEvent();
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.UpdateEpisodes(It.Is<List<Episode>>(l => l.All(e => e.Monitored))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_monitor_missing_episodes_only()
|
||||
{
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = false
|
||||
});
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
|
||||
VerifyMonitored(e => !e.HasFile);
|
||||
VerifyNotMonitored(e => e.HasFile);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_able_to_monitor_new_episodes_only()
|
||||
{
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = true
|
||||
});
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
|
||||
VerifyMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.After(DateTime.UtcNow));
|
||||
VerifyMonitored(e => !e.AirDateUtc.HasValue);
|
||||
VerifyNotMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_monitor_missing_specials()
|
||||
{
|
||||
GivenSpecials();
|
||||
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = false
|
||||
});
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
|
||||
VerifyMonitored(e => !e.HasFile);
|
||||
VerifyNotMonitored(e => e.HasFile);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_monitor_new_specials()
|
||||
{
|
||||
GivenSpecials();
|
||||
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = true
|
||||
});
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
VerifyMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.After(DateTime.UtcNow));
|
||||
VerifyMonitored(e => !e.AirDateUtc.HasValue);
|
||||
VerifyNotMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_monitor_season_when_all_episodes_are_monitored_except_latest_season()
|
||||
{
|
||||
_series.Seasons = Builder<Season>.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(n => n.Monitored = true)
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
_episodes = Builder<Episode>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(e => e.SeasonNumber = 1)
|
||||
.With(e => e.EpisodeFileId = 0)
|
||||
.With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-5))
|
||||
.TheLast(1)
|
||||
.With(e => e.SeasonNumber = 2)
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Setup(s => s.GetEpisodeBySeries(It.IsAny<int>()))
|
||||
.Returns(_episodes);
|
||||
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
{
|
||||
IgnoreEpisodesWithoutFiles = true
|
||||
});
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
|
||||
VerifySeasonMonitored(n => n.SeasonNumber == 2);
|
||||
VerifySeasonNotMonitored(n => n.SeasonNumber == 1);
|
||||
}
|
||||
|
||||
private void VerifyMonitored(Func<Episode, bool> predicate)
|
||||
{
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.UpdateEpisodes(It.Is<List<Episode>>(l => l.Where(predicate).All(e => e.Monitored))));
|
||||
}
|
||||
|
||||
private void VerifyNotMonitored(Func<Episode, bool> predicate)
|
||||
{
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.UpdateEpisodes(It.Is<List<Episode>>(l => l.Where(predicate).All(e => !e.Monitored))));
|
||||
}
|
||||
|
||||
private void VerifySeasonMonitored(Func<Season, bool> predicate)
|
||||
{
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.Seasons.Where(predicate).All(n => n.Monitored))));
|
||||
}
|
||||
|
||||
private void VerifySeasonNotMonitored(Func<Season, bool> predicate)
|
||||
{
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.Seasons.Where(predicate).All(n => !n.Monitored))));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(77)]
|
||||
public class add_add_options_to_series : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Series").AddColumn("AddOptions").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ using NzbDrone.Common;
|
|||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
@ -66,6 +67,36 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
_logger.ProgressInfo("Completed search for {0} episodes. {1} reports downloaded.", missing.Count, downloadedCount);
|
||||
}
|
||||
|
||||
private void SearchForMissingEpisodes(List<Episode> episodes)
|
||||
{
|
||||
_logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count);
|
||||
var downloadedCount = 0;
|
||||
|
||||
foreach (var series in episodes.GroupBy(e => e.SeriesId))
|
||||
{
|
||||
foreach (var season in series.Select(e => e).GroupBy(e => e.SeasonNumber))
|
||||
{
|
||||
List<DownloadDecision> decisions;
|
||||
|
||||
if (season.Count() > 1)
|
||||
{
|
||||
decisions = _nzbSearchService.SeasonSearch(series.Key, season.Key);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
decisions = _nzbSearchService.EpisodeSearch(season.First());
|
||||
}
|
||||
|
||||
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
|
||||
|
||||
downloadedCount += processed.Grabbed.Count;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", episodes.Count, downloadedCount);
|
||||
}
|
||||
|
||||
public void Execute(EpisodeSearchCommand message)
|
||||
{
|
||||
foreach (var episodeId in message.EpisodeIds)
|
||||
|
@ -79,36 +110,34 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
|
||||
public void Execute(MissingEpisodeSearchCommand message)
|
||||
{
|
||||
//TODO: Look at ways to make this more efficient (grouping by series/season)
|
||||
List<Episode> episodes;
|
||||
|
||||
var episodes =
|
||||
_episodeService.EpisodesWithoutFiles(new PagingSpec<Episode>
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 100000,
|
||||
SortDirection = SortDirection.Ascending,
|
||||
SortKey = "Id",
|
||||
FilterExpression = v => v.Monitored == true && v.Series.Monitored == true
|
||||
}).Records.ToList();
|
||||
|
||||
var missing = episodes.Where(e => !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)).ToList();
|
||||
|
||||
_logger.ProgressInfo("Performing missing search for {0} episodes", missing.Count);
|
||||
var downloadedCount = 0;
|
||||
|
||||
//Limit requests to indexers at 100 per minute
|
||||
using (var rateGate = new RateGate(100, TimeSpan.FromSeconds(60)))
|
||||
if (message.SeriesId > 0)
|
||||
{
|
||||
foreach (var episode in missing)
|
||||
{
|
||||
rateGate.WaitToProceed();
|
||||
var decisions = _nzbSearchService.EpisodeSearch(episode);
|
||||
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
|
||||
downloadedCount += processed.Grabbed.Count;
|
||||
}
|
||||
episodes = _episodeService.GetEpisodeBySeries(message.SeriesId)
|
||||
.Where(e => e.Monitored && !e.HasFile)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
_logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", missing.Count, downloadedCount);
|
||||
else
|
||||
{
|
||||
episodes = _episodeService.EpisodesWithoutFiles(new PagingSpec<Episode>
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 100000,
|
||||
SortDirection = SortDirection.Ascending,
|
||||
SortKey = "Id",
|
||||
FilterExpression =
|
||||
v =>
|
||||
v.Monitored == true &&
|
||||
v.Series.Monitored == true
|
||||
}).Records.ToList();
|
||||
}
|
||||
|
||||
var queue = _queueService.GetQueue().Select(q => q.Episode.Id);
|
||||
var missing = episodes.Where(e => !queue.Contains(e.Id)).ToList();
|
||||
|
||||
SearchForMissingEpisodes(missing);
|
||||
}
|
||||
|
||||
public void Handle(EpisodeInfoRefreshedEvent message)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class MissingEpisodeSearchCommand : Command
|
||||
{
|
||||
public List<int> EpisodeIds { get; set; }
|
||||
public int SeriesId { get; private set; }
|
||||
|
||||
public override bool SendUpdatesToClient
|
||||
{
|
||||
|
@ -19,9 +18,9 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
{
|
||||
}
|
||||
|
||||
public MissingEpisodeSearchCommand(List<int> episodeIds)
|
||||
public MissingEpisodeSearchCommand(int seriesId)
|
||||
{
|
||||
EpisodeIds = episodeIds;
|
||||
SeriesId = seriesId;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
if (!_diskProvider.FolderExists(rootFolder))
|
||||
{
|
||||
_logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder);
|
||||
_eventAggregator.PublishEvent(new SeriesScanSkippedEvent(series, SeriesScanSkippedReason.RootFolderDoesNotExist));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -92,6 +93,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
_logger.Debug("Series folder doesn't exist: {0}", series.Path);
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new SeriesScanSkippedEvent(series, SeriesScanSkippedReason.SeriesFolderDoesNotExist));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.Events
|
||||
{
|
||||
public class SeriesScanSkippedEvent : IEvent
|
||||
{
|
||||
public Series Series { get; private set; }
|
||||
public SeriesScanSkippedReason Reason { get; set; }
|
||||
|
||||
public SeriesScanSkippedEvent(Series series, SeriesScanSkippedReason reason)
|
||||
{
|
||||
Series = series;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SeriesScanSkippedReason
|
||||
{
|
||||
RootFolderDoesNotExist,
|
||||
SeriesFolderDoesNotExist
|
||||
}
|
||||
}
|
|
@ -245,6 +245,7 @@
|
|||
<Compile Include="Datastore\Migration\075_force_lib_update.cs" />
|
||||
<Compile Include="Datastore\Migration\074_disable_eztv.cs" />
|
||||
<Compile Include="Datastore\Migration\073_clear_ratings.cs" />
|
||||
<Compile Include="Datastore\Migration\077_add_add_options_to_series.cs" />
|
||||
<Compile Include="Datastore\Migration\070_delay_profile.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
||||
|
@ -571,6 +572,7 @@
|
|||
<Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\SeriesScanSkippedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
|
||||
<Compile Include="MediaFiles\FileDateType.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileAttributeService.cs" />
|
||||
|
@ -817,6 +819,7 @@
|
|||
<Compile Include="ThingiProvider\ProviderFactory.cs" />
|
||||
<Compile Include="ThingiProvider\ProviderRepository.cs" />
|
||||
<Compile Include="Tv\Actor.cs" />
|
||||
<Compile Include="Tv\AddSeriesOptions.cs" />
|
||||
<Compile Include="Tv\Commands\MoveSeriesCommand.cs" />
|
||||
<Compile Include="Tv\Commands\RefreshSeriesCommand.cs" />
|
||||
<Compile Include="Tv\Episode.cs" />
|
||||
|
@ -838,6 +841,7 @@
|
|||
<Compile Include="Tv\RefreshSeriesService.cs" />
|
||||
<Compile Include="Tv\Season.cs" />
|
||||
<Compile Include="Tv\Series.cs" />
|
||||
<Compile Include="Tv\SeriesScannedHandler.cs" />
|
||||
<Compile Include="Tv\SeriesEditedService.cs" />
|
||||
<Compile Include="Tv\SeriesRepository.cs" />
|
||||
<Compile Include="Tv\SeriesService.cs">
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class AddSeriesOptions : IEmbeddedDocument
|
||||
{
|
||||
public bool SearchForMissingEpisodes { get; set; }
|
||||
public bool IgnoreEpisodesWithFiles { get; set; }
|
||||
public bool IgnoreEpisodesWithoutFiles { get; set; }
|
||||
}
|
||||
}
|
|
@ -171,4 +171,4 @@ namespace NzbDrone.Core.Tv
|
|||
RefreshSeriesInfo(message.Series);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ namespace NzbDrone.Core.Tv
|
|||
|
||||
public List<Season> Seasons { get; set; }
|
||||
public HashSet<Int32> Tags { get; set; }
|
||||
public AddSeriesOptions AddOptions { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class SeriesScannedHandler : IHandle<SeriesScannedEvent>,
|
||||
IHandle<SeriesScanSkippedEvent>
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ICommandExecutor _commandExecutor;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SeriesScannedHandler(ISeriesService seriesService,
|
||||
IEpisodeService episodeService,
|
||||
ICommandExecutor commandExecutor,
|
||||
Logger logger)
|
||||
{
|
||||
_seriesService = seriesService;
|
||||
_episodeService = episodeService;
|
||||
_commandExecutor = commandExecutor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private void SetEpisodeMonitoredStatus(Series series, List<Episode> episodes)
|
||||
{
|
||||
_logger.Debug("[{0}] Setting episode monitored status.", series.Title);
|
||||
|
||||
if (series.AddOptions.IgnoreEpisodesWithFiles)
|
||||
{
|
||||
_logger.Debug("Ignoring Episodes with Files");
|
||||
UnmonitorEpisodes(episodes.Where(e => e.HasFile));
|
||||
}
|
||||
|
||||
if (series.AddOptions.IgnoreEpisodesWithoutFiles)
|
||||
{
|
||||
_logger.Debug("Ignoring Episodes without Files");
|
||||
UnmonitorEpisodes(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault();
|
||||
|
||||
foreach (var season in series.Seasons.Where(s => s.SeasonNumber < lastSeason))
|
||||
{
|
||||
if (episodes.Where(e => e.SeasonNumber == season.SeasonNumber).All(e => !e.Monitored))
|
||||
{
|
||||
season.Monitored = false;
|
||||
}
|
||||
}
|
||||
|
||||
_seriesService.UpdateSeries(series);
|
||||
_episodeService.UpdateEpisodes(episodes);
|
||||
}
|
||||
|
||||
private void UnmonitorEpisodes(IEnumerable<Episode> episodes)
|
||||
{
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
episode.Monitored = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleScanEvents(Series series)
|
||||
{
|
||||
if (series.AddOptions == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Info("[{0}] was recently added, performing post-add actions", series.Title);
|
||||
|
||||
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
SetEpisodeMonitoredStatus(series, episodes);
|
||||
|
||||
if (series.AddOptions.SearchForMissingEpisodes)
|
||||
{
|
||||
_commandExecutor.PublishCommand(new MissingEpisodeSearchCommand(series.Id));
|
||||
}
|
||||
|
||||
series.AddOptions = null;
|
||||
_seriesService.RemoveAddOptions(series);
|
||||
}
|
||||
|
||||
public void Handle(SeriesScannedEvent message)
|
||||
{
|
||||
HandleScanEvents(message.Series);
|
||||
}
|
||||
|
||||
public void Handle(SeriesScanSkippedEvent message)
|
||||
{
|
||||
HandleScanEvents(message.Series);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
@ -27,6 +28,7 @@ namespace NzbDrone.Core.Tv
|
|||
Series UpdateSeries(Series series);
|
||||
List<Series> UpdateSeries(List<Series> series);
|
||||
bool SeriesPathExists(string folder);
|
||||
void RemoveAddOptions(Series series);
|
||||
}
|
||||
|
||||
public class SeriesService : ISeriesService
|
||||
|
@ -216,5 +218,10 @@ namespace NzbDrone.Core.Tv
|
|||
{
|
||||
return _seriesRepository.SeriesPathExists(folder);
|
||||
}
|
||||
|
||||
public void RemoveAddOptions(Series series)
|
||||
{
|
||||
_seriesRepository.SetFields(series, s => s.AddOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<dl class="monitor-tooltip-contents">
|
||||
<dt>All</dt>
|
||||
<dd>Monitor all episodes except specials</dd>
|
||||
<dt>Future</dt>
|
||||
<dd>Monitor episodes that have not aired yet</dd>
|
||||
<dt>Missing</dt>
|
||||
<dd>Monitor episodes that do not have files or have not aired yet</dd>
|
||||
<dt>Existing</dt>
|
||||
<dd>Monitor episodes that have files or have not aired yet</dd>
|
||||
<dt>First Season</dt>
|
||||
<dd>Monitor all episodes of the first season. All other seasons will be ignored</dd>
|
||||
<!--<dt>Latest Season</dt>-->
|
||||
<!--<dd>Monitor all episodes the latest season only, previous seasons will be ignored</dd>-->
|
||||
</dl>
|
|
@ -36,17 +36,20 @@ define(
|
|||
rootFolder : '.x-root-folder',
|
||||
seasonFolder : '.x-season-folder',
|
||||
seriesType : '.x-series-type',
|
||||
startingSeason : '.x-starting-season',
|
||||
monitor : '.x-monitor',
|
||||
monitorTooltip : '.x-monitor-tooltip',
|
||||
addButton : '.x-add',
|
||||
overview : '.x-overview'
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .x-add' : '_addSeries',
|
||||
'click .x-add' : '_addWithoutSearch',
|
||||
'click .x-add-search' : '_addAndSearch',
|
||||
'change .x-profile' : '_profileChanged',
|
||||
'change .x-root-folder' : '_rootFolderChanged',
|
||||
'change .x-season-folder' : '_seasonFolderChanged',
|
||||
'change .x-series-type' : '_seriesTypeChanged'
|
||||
'change .x-series-type' : '_seriesTypeChanged',
|
||||
'change .x-monitor' : '_monitorChanged'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
|
@ -69,6 +72,7 @@ define(
|
|||
var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId);
|
||||
var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true);
|
||||
var defaultSeriesType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard');
|
||||
var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing');
|
||||
|
||||
if (Profiles.get(defaultProfile)) {
|
||||
this.ui.profile.val(defaultProfile);
|
||||
|
@ -80,18 +84,25 @@ define(
|
|||
|
||||
this.ui.seasonFolder.prop('checked', useSeasonFolder);
|
||||
this.ui.seriesType.val(defaultSeriesType);
|
||||
|
||||
var minSeasonNotZero = _.min(_.reject(this.model.get('seasons'), { seasonNumber: 0 }), 'seasonNumber');
|
||||
|
||||
if (minSeasonNotZero) {
|
||||
this.ui.startingSeason.val(minSeasonNotZero.seasonNumber);
|
||||
}
|
||||
this.ui.monitor.val(defaultMonitorEpisodes);
|
||||
|
||||
//TODO: make this work via onRender, FM?
|
||||
//works with onShow, but stops working after the first render
|
||||
this.ui.overview.dotdotdot({
|
||||
height: 120
|
||||
});
|
||||
|
||||
this.templateFunction = Marionette.TemplateCache.get('AddSeries/MonitoringTooltipTemplate');
|
||||
var content = this.templateFunction();
|
||||
|
||||
this.ui.monitorTooltip.popover({
|
||||
content : content,
|
||||
html : true,
|
||||
trigger : 'hover',
|
||||
title : 'Episode Monitoring Options',
|
||||
placement: 'right',
|
||||
container: this.$el
|
||||
});
|
||||
},
|
||||
|
||||
_configureTemplateHelpers: function () {
|
||||
|
@ -124,6 +135,10 @@ define(
|
|||
else if (options.key === Config.Keys.DefaultSeriesType) {
|
||||
this.ui.seriesType.val(options.value);
|
||||
}
|
||||
|
||||
else if (options.key === Config.Keys.MonitorEpisodes) {
|
||||
this.ui.monitor.val(options.value);
|
||||
}
|
||||
},
|
||||
|
||||
_profileChanged: function () {
|
||||
|
@ -150,30 +165,43 @@ define(
|
|||
Config.setValue(Config.Keys.DefaultSeriesType, this.ui.seriesType.val());
|
||||
},
|
||||
|
||||
_monitorChanged: function () {
|
||||
Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val());
|
||||
},
|
||||
|
||||
_setRootFolder: function (options) {
|
||||
vent.trigger(vent.Commands.CloseModalCommand);
|
||||
this.ui.rootFolder.val(options.model.id);
|
||||
this._rootFolderChanged();
|
||||
},
|
||||
|
||||
_addSeries: function () {
|
||||
_addWithoutSearch: function () {
|
||||
this._addSeries(false);
|
||||
},
|
||||
|
||||
_addAndSearch: function() {
|
||||
this._addSeries(true);
|
||||
},
|
||||
|
||||
_addSeries: function (searchForMissingEpisodes) {
|
||||
var icon = this.ui.addButton.find('icon');
|
||||
icon.removeClass('icon-plus').addClass('icon-spin icon-spinner disabled');
|
||||
|
||||
var profile = this.ui.profile.val();
|
||||
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
|
||||
var startingSeason = this.ui.startingSeason.val();
|
||||
var seriesType = this.ui.seriesType.val();
|
||||
var seasonFolder = this.ui.seasonFolder.prop('checked');
|
||||
|
||||
this.model.set({
|
||||
profileId: profile,
|
||||
rootFolderPath: rootFolderPath,
|
||||
seasonFolder: seasonFolder,
|
||||
seriesType: seriesType
|
||||
}, { silent: true });
|
||||
var options = this._getAddSeriesOptions();
|
||||
options.searchForMissingEpisodes = searchForMissingEpisodes;
|
||||
|
||||
this.model.setSeasonPass(startingSeason);
|
||||
this.model.set({
|
||||
profileId : profile,
|
||||
rootFolderPath : rootFolderPath,
|
||||
seasonFolder : seasonFolder,
|
||||
seriesType : seriesType,
|
||||
addOptions : options
|
||||
}, { silent: true });
|
||||
|
||||
var self = this;
|
||||
var promise = this.model.save();
|
||||
|
@ -209,6 +237,48 @@ define(
|
|||
_rootFoldersUpdated: function () {
|
||||
this._configureTemplateHelpers();
|
||||
this.render();
|
||||
},
|
||||
|
||||
_getAddSeriesOptions: function () {
|
||||
var monitor = this.ui.monitor.val();
|
||||
var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber');
|
||||
var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber: 0 }), 'seasonNumber');
|
||||
|
||||
this.model.setSeasonPass(firstSeason.seasonNumber);
|
||||
|
||||
var options = {
|
||||
ignoreEpisodesWithFiles: false,
|
||||
ignoreEpisodesWithoutFiles: false
|
||||
};
|
||||
|
||||
if (monitor === 'all') {
|
||||
return options;
|
||||
}
|
||||
|
||||
else if (monitor === 'future') {
|
||||
options.ignoreEpisodesWithFiles = true;
|
||||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'latest') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber);
|
||||
}
|
||||
|
||||
else if (monitor === 'first') {
|
||||
this.model.setSeasonPass(lastSeason + 1);
|
||||
|
||||
firstSeason.monitor = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'missing') {
|
||||
options.ignoreEpisodesWithFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'existing') {
|
||||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -35,8 +35,15 @@
|
|||
{{/unless}}
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label>Starting Season</label>
|
||||
{{> StartingSeasonSelectionPartial seasons}}
|
||||
<label>Monitor <i class="icon-nd-form-info monitor-tooltip x-monitor-tooltip"></i></label>
|
||||
<select class="form-control col-md-2 x-monitor">
|
||||
<option value="all">All</option>
|
||||
<option value="future">Future</option>
|
||||
<option value="missing">Missing</option>
|
||||
<option value="existing">Existing</option>
|
||||
<!--<option value="latest">Latest Season</option>-->
|
||||
<option value="first">First Season</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
|
@ -70,10 +77,16 @@
|
|||
{{#if title}}
|
||||
<div class="form-group col-md-2 col-md-offset-10">
|
||||
<!--Uncomment if we need to add even more controls to add series-->
|
||||
<!--<label> </label>-->
|
||||
<button class="btn btn-success x-add"> Add
|
||||
<i class="icon-plus"></i>
|
||||
</button>
|
||||
<!--<label style="visibility: hidden">Add</label>-->
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success add x-add">
|
||||
<i class="icon-plus" title="Add"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success add x-add-search">
|
||||
<i class="icon-search" title="Add and Search for missing episodes"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="col-md-2 col-md-offset-10" title="Series requires an English title">
|
||||
|
|
|
@ -84,18 +84,24 @@
|
|||
}
|
||||
|
||||
select {
|
||||
font-size : 16px;
|
||||
font-size : 14px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-top : 0px;
|
||||
}
|
||||
|
||||
i {
|
||||
&:before {
|
||||
color : #ffffff;
|
||||
.add {
|
||||
i {
|
||||
&:before {
|
||||
color : #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-tooltip {
|
||||
margin-left : 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-folders {
|
||||
|
@ -107,6 +113,14 @@
|
|||
color : #999999;
|
||||
font-style : italic;
|
||||
}
|
||||
|
||||
.monitor-tooltip-contents {
|
||||
padding-bottom : 0px;
|
||||
|
||||
dd {
|
||||
padding-bottom : 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li.add-new {
|
||||
|
|
|
@ -10,10 +10,11 @@ define(
|
|||
|
||||
Keys : {
|
||||
DefaultProfileId : 'DefaultProfileId',
|
||||
DefaultRootFolderId : 'DefaultRootFolderId',
|
||||
UseSeasonFolder : 'UseSeasonFolder',
|
||||
DefaultSeriesType : 'DefaultSeriesType',
|
||||
AdvancedSettings : 'advancedSettings'
|
||||
DefaultRootFolderId : 'DefaultRootFolderId',
|
||||
UseSeasonFolder : 'UseSeasonFolder',
|
||||
DefaultSeriesType : 'DefaultSeriesType',
|
||||
MonitorEpisodes : 'MonitorEpisodes',
|
||||
AdvancedSettings : 'advancedSettings'
|
||||
},
|
||||
|
||||
getValueBoolean: function (key, defaultValue) {
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
i {
|
||||
margin-right : 0px;
|
||||
color : inherit;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size : 16px;
|
||||
color : #595959;
|
||||
|
|
Loading…
Reference in New Issue