New: options when adding series, including the ability to search for all missing episodes

pull/4/head
Mark McDowall 10 years ago
parent 7b7f7ac56b
commit 05ee57a972

@ -16,6 +16,7 @@ using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Omu.ValueInjecter;
namespace NzbDrone.Api.Series namespace NzbDrone.Api.Series
{ {
@ -109,7 +110,9 @@ namespace NzbDrone.Api.Series
private int AddSeries(SeriesResource seriesResource) 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) private void UpdateSeries(SeriesResource seriesResource)

@ -67,6 +67,7 @@ namespace NzbDrone.Api.Series
public List<String> Genres { get; set; } public List<String> Genres { get; set; }
public HashSet<Int32> Tags { get; set; } public HashSet<Int32> Tags { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public AddSeriesOptions AddOptions { get; set; }
//Used to support legacy consumers //Used to support legacy consumers
public Int32 QualityProfileId public Int32 QualityProfileId

@ -317,6 +317,7 @@
<Compile Include="TvTests\MoveSeriesServiceFixture.cs" /> <Compile Include="TvTests\MoveSeriesServiceFixture.cs" />
<Compile Include="TvTests\RefreshEpisodeServiceFixture.cs" /> <Compile Include="TvTests\RefreshEpisodeServiceFixture.cs" />
<Compile Include="TvTests\RefreshSeriesServiceFixture.cs" /> <Compile Include="TvTests\RefreshSeriesServiceFixture.cs" />
<Compile Include="TvTests\SeriesAddedHandlerTests\SetEpisodeMontitoredFixture.cs" />
<Compile Include="TvTests\SeriesRepositoryTests\SeriesRepositoryFixture.cs" /> <Compile Include="TvTests\SeriesRepositoryTests\SeriesRepositoryFixture.cs" />
<Compile Include="TvTests\SeriesServiceTests\AddSeriesFixture.cs" /> <Compile Include="TvTests\SeriesServiceTests\AddSeriesFixture.cs" />
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.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.Extensions;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; 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); _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) public void Execute(EpisodeSearchCommand message)
{ {
foreach (var episodeId in message.EpisodeIds) foreach (var episodeId in message.EpisodeIds)
@ -79,36 +110,34 @@ namespace NzbDrone.Core.IndexerSearch
public void Execute(MissingEpisodeSearchCommand message) public void Execute(MissingEpisodeSearchCommand message)
{ {
//TODO: Look at ways to make this more efficient (grouping by series/season) List<Episode> episodes;
if (message.SeriesId > 0)
{
episodes = _episodeService.GetEpisodeBySeries(message.SeriesId)
.Where(e => e.Monitored && !e.HasFile)
.ToList();
}
var episodes = else
_episodeService.EpisodesWithoutFiles(new PagingSpec<Episode> {
episodes = _episodeService.EpisodesWithoutFiles(new PagingSpec<Episode>
{ {
Page = 1, Page = 1,
PageSize = 100000, PageSize = 100000,
SortDirection = SortDirection.Ascending, SortDirection = SortDirection.Ascending,
SortKey = "Id", SortKey = "Id",
FilterExpression = v => v.Monitored == true && v.Series.Monitored == true FilterExpression =
v =>
v.Monitored == true &&
v.Series.Monitored == true
}).Records.ToList(); }).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)))
{
foreach (var episode in missing)
{
rateGate.WaitToProceed();
var decisions = _nzbSearchService.EpisodeSearch(episode);
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
downloadedCount += processed.Grabbed.Count;
}
} }
_logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", missing.Count, downloadedCount); 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) 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 namespace NzbDrone.Core.IndexerSearch
{ {
public class MissingEpisodeSearchCommand : Command public class MissingEpisodeSearchCommand : Command
{ {
public List<int> EpisodeIds { get; set; } public int SeriesId { get; private set; }
public override bool SendUpdatesToClient 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)) if (!_diskProvider.FolderExists(rootFolder))
{ {
_logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder);
_eventAggregator.PublishEvent(new SeriesScanSkippedEvent(series, SeriesScanSkippedReason.RootFolderDoesNotExist));
return; return;
} }
@ -92,6 +93,7 @@ namespace NzbDrone.Core.MediaFiles
_logger.Debug("Series folder doesn't exist: {0}", series.Path); _logger.Debug("Series folder doesn't exist: {0}", series.Path);
} }
_eventAggregator.PublishEvent(new SeriesScanSkippedEvent(series, SeriesScanSkippedReason.SeriesFolderDoesNotExist));
return; 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\075_force_lib_update.cs" />
<Compile Include="Datastore\Migration\074_disable_eztv.cs" /> <Compile Include="Datastore\Migration\074_disable_eztv.cs" />
<Compile Include="Datastore\Migration\073_clear_ratings.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\070_delay_profile.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
@ -571,6 +572,7 @@
<Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" />
<Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesScanSkippedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
<Compile Include="MediaFiles\FileDateType.cs" /> <Compile Include="MediaFiles\FileDateType.cs" />
<Compile Include="MediaFiles\MediaFileAttributeService.cs" /> <Compile Include="MediaFiles\MediaFileAttributeService.cs" />
@ -817,6 +819,7 @@
<Compile Include="ThingiProvider\ProviderFactory.cs" /> <Compile Include="ThingiProvider\ProviderFactory.cs" />
<Compile Include="ThingiProvider\ProviderRepository.cs" /> <Compile Include="ThingiProvider\ProviderRepository.cs" />
<Compile Include="Tv\Actor.cs" /> <Compile Include="Tv\Actor.cs" />
<Compile Include="Tv\AddSeriesOptions.cs" />
<Compile Include="Tv\Commands\MoveSeriesCommand.cs" /> <Compile Include="Tv\Commands\MoveSeriesCommand.cs" />
<Compile Include="Tv\Commands\RefreshSeriesCommand.cs" /> <Compile Include="Tv\Commands\RefreshSeriesCommand.cs" />
<Compile Include="Tv\Episode.cs" /> <Compile Include="Tv\Episode.cs" />
@ -838,6 +841,7 @@
<Compile Include="Tv\RefreshSeriesService.cs" /> <Compile Include="Tv\RefreshSeriesService.cs" />
<Compile Include="Tv\Season.cs" /> <Compile Include="Tv\Season.cs" />
<Compile Include="Tv\Series.cs" /> <Compile Include="Tv\Series.cs" />
<Compile Include="Tv\SeriesScannedHandler.cs" />
<Compile Include="Tv\SeriesEditedService.cs" /> <Compile Include="Tv\SeriesEditedService.cs" />
<Compile Include="Tv\SeriesRepository.cs" /> <Compile Include="Tv\SeriesRepository.cs" />
<Compile Include="Tv\SeriesService.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; }
}
}

@ -49,6 +49,7 @@ namespace NzbDrone.Core.Tv
public List<Season> Seasons { get; set; } public List<Season> Seasons { get; set; }
public HashSet<Int32> Tags { get; set; } public HashSet<Int32> Tags { get; set; }
public AddSeriesOptions AddOptions { get; set; }
public override string ToString() 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.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -27,6 +28,7 @@ namespace NzbDrone.Core.Tv
Series UpdateSeries(Series series); Series UpdateSeries(Series series);
List<Series> UpdateSeries(List<Series> series); List<Series> UpdateSeries(List<Series> series);
bool SeriesPathExists(string folder); bool SeriesPathExists(string folder);
void RemoveAddOptions(Series series);
} }
public class SeriesService : ISeriesService public class SeriesService : ISeriesService
@ -216,5 +218,10 @@ namespace NzbDrone.Core.Tv
{ {
return _seriesRepository.SeriesPathExists(folder); 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', rootFolder : '.x-root-folder',
seasonFolder : '.x-season-folder', seasonFolder : '.x-season-folder',
seriesType : '.x-series-type', seriesType : '.x-series-type',
startingSeason : '.x-starting-season', monitor : '.x-monitor',
monitorTooltip : '.x-monitor-tooltip',
addButton : '.x-add', addButton : '.x-add',
overview : '.x-overview' overview : '.x-overview'
}, },
events: { events: {
'click .x-add' : '_addSeries', 'click .x-add' : '_addWithoutSearch',
'click .x-add-search' : '_addAndSearch',
'change .x-profile' : '_profileChanged', 'change .x-profile' : '_profileChanged',
'change .x-root-folder' : '_rootFolderChanged', 'change .x-root-folder' : '_rootFolderChanged',
'change .x-season-folder' : '_seasonFolderChanged', 'change .x-season-folder' : '_seasonFolderChanged',
'change .x-series-type' : '_seriesTypeChanged' 'change .x-series-type' : '_seriesTypeChanged',
'change .x-monitor' : '_monitorChanged'
}, },
initialize: function () { initialize: function () {
@ -69,6 +72,7 @@ define(
var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId);
var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true); var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true);
var defaultSeriesType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard'); var defaultSeriesType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard');
var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing');
if (Profiles.get(defaultProfile)) { if (Profiles.get(defaultProfile)) {
this.ui.profile.val(defaultProfile); this.ui.profile.val(defaultProfile);
@ -80,18 +84,25 @@ define(
this.ui.seasonFolder.prop('checked', useSeasonFolder); this.ui.seasonFolder.prop('checked', useSeasonFolder);
this.ui.seriesType.val(defaultSeriesType); this.ui.seriesType.val(defaultSeriesType);
this.ui.monitor.val(defaultMonitorEpisodes);
var minSeasonNotZero = _.min(_.reject(this.model.get('seasons'), { seasonNumber: 0 }), 'seasonNumber');
if (minSeasonNotZero) {
this.ui.startingSeason.val(minSeasonNotZero.seasonNumber);
}
//TODO: make this work via onRender, FM? //TODO: make this work via onRender, FM?
//works with onShow, but stops working after the first render //works with onShow, but stops working after the first render
this.ui.overview.dotdotdot({ this.ui.overview.dotdotdot({
height: 120 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 () { _configureTemplateHelpers: function () {
@ -124,6 +135,10 @@ define(
else if (options.key === Config.Keys.DefaultSeriesType) { else if (options.key === Config.Keys.DefaultSeriesType) {
this.ui.seriesType.val(options.value); this.ui.seriesType.val(options.value);
} }
else if (options.key === Config.Keys.MonitorEpisodes) {
this.ui.monitor.val(options.value);
}
}, },
_profileChanged: function () { _profileChanged: function () {
@ -150,31 +165,44 @@ define(
Config.setValue(Config.Keys.DefaultSeriesType, this.ui.seriesType.val()); Config.setValue(Config.Keys.DefaultSeriesType, this.ui.seriesType.val());
}, },
_monitorChanged: function () {
Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val());
},
_setRootFolder: function (options) { _setRootFolder: function (options) {
vent.trigger(vent.Commands.CloseModalCommand); vent.trigger(vent.Commands.CloseModalCommand);
this.ui.rootFolder.val(options.model.id); this.ui.rootFolder.val(options.model.id);
this._rootFolderChanged(); this._rootFolderChanged();
}, },
_addSeries: function () { _addWithoutSearch: function () {
this._addSeries(false);
},
_addAndSearch: function() {
this._addSeries(true);
},
_addSeries: function (searchForMissingEpisodes) {
var icon = this.ui.addButton.find('icon'); var icon = this.ui.addButton.find('icon');
icon.removeClass('icon-plus').addClass('icon-spin icon-spinner disabled'); icon.removeClass('icon-plus').addClass('icon-spin icon-spinner disabled');
var profile = this.ui.profile.val(); var profile = this.ui.profile.val();
var rootFolderPath = this.ui.rootFolder.children(':selected').text(); var rootFolderPath = this.ui.rootFolder.children(':selected').text();
var startingSeason = this.ui.startingSeason.val();
var seriesType = this.ui.seriesType.val(); var seriesType = this.ui.seriesType.val();
var seasonFolder = this.ui.seasonFolder.prop('checked'); var seasonFolder = this.ui.seasonFolder.prop('checked');
var options = this._getAddSeriesOptions();
options.searchForMissingEpisodes = searchForMissingEpisodes;
this.model.set({ this.model.set({
profileId: profile, profileId : profile,
rootFolderPath: rootFolderPath, rootFolderPath : rootFolderPath,
seasonFolder: seasonFolder, seasonFolder : seasonFolder,
seriesType: seriesType seriesType : seriesType,
addOptions : options
}, { silent: true }); }, { silent: true });
this.model.setSeasonPass(startingSeason);
var self = this; var self = this;
var promise = this.model.save(); var promise = this.model.save();
@ -209,6 +237,48 @@ define(
_rootFoldersUpdated: function () { _rootFoldersUpdated: function () {
this._configureTemplateHelpers(); this._configureTemplateHelpers();
this.render(); 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}} {{/unless}}
<div class="form-group col-md-2"> <div class="form-group col-md-2">
<label>Starting Season</label> <label>Monitor <i class="icon-nd-form-info monitor-tooltip x-monitor-tooltip"></i></label>
{{> StartingSeasonSelectionPartial seasons}} <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>
<div class="form-group col-md-2"> <div class="form-group col-md-2">
@ -70,10 +77,16 @@
{{#if title}} {{#if title}}
<div class="form-group col-md-2 col-md-offset-10"> <div class="form-group col-md-2 col-md-offset-10">
<!--Uncomment if we need to add even more controls to add series--> <!--Uncomment if we need to add even more controls to add series-->
<!--<label>&nbsp;</label>--> <!--<label style="visibility: hidden">Add</label>-->
<button class="btn btn-success x-add"> Add <div class="btn-group">
<i class="icon-plus"></i> <button class="btn btn-success add x-add">
<i class="icon-plus" title="Add"></i>
</button> </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> </div>
{{else}} {{else}}
<div class="col-md-2 col-md-offset-10" title="Series requires an English title"> <div class="col-md-2 col-md-offset-10" title="Series requires an English title">

@ -84,13 +84,14 @@
} }
select { select {
font-size : 16px; font-size : 14px;
} }
.checkbox { .checkbox {
margin-top : 0px; margin-top : 0px;
} }
.add {
i { i {
&:before { &:before {
color : #ffffff; color : #ffffff;
@ -98,6 +99,11 @@
} }
} }
.monitor-tooltip {
margin-left : 5px;
}
}
.loading-folders { .loading-folders {
margin : 30px 0px; margin : 30px 0px;
text-align: center; text-align: center;
@ -107,6 +113,14 @@
color : #999999; color : #999999;
font-style : italic; font-style : italic;
} }
.monitor-tooltip-contents {
padding-bottom : 0px;
dd {
padding-bottom : 8px;
}
}
} }
li.add-new { li.add-new {

@ -13,6 +13,7 @@ define(
DefaultRootFolderId : 'DefaultRootFolderId', DefaultRootFolderId : 'DefaultRootFolderId',
UseSeasonFolder : 'UseSeasonFolder', UseSeasonFolder : 'UseSeasonFolder',
DefaultSeriesType : 'DefaultSeriesType', DefaultSeriesType : 'DefaultSeriesType',
MonitorEpisodes : 'MonitorEpisodes',
AdvancedSettings : 'advancedSettings' AdvancedSettings : 'advancedSettings'
}, },

@ -26,6 +26,13 @@
} }
} }
.btn {
i {
margin-right : 0px;
color : inherit;
}
}
i { i {
font-size : 16px; font-size : 16px;
color : #595959; color : #595959;

Loading…
Cancel
Save