mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-17 15:02:34 +02:00
Season pass overhaul
New: Season Pass supports multi-select New: Improved Season Pass toggling Closes #396
This commit is contained in:
parent
28e2cf97da
commit
155c82c199
@ -10,6 +10,8 @@ public static class ValueInjectorExtensions
|
||||
{
|
||||
public static TTarget InjectTo<TTarget>(this object source) where TTarget : new()
|
||||
{
|
||||
if (source == null) return default(TTarget);
|
||||
|
||||
var targetType = typeof(TTarget);
|
||||
|
||||
if (targetType.IsGenericType &&
|
||||
|
@ -216,12 +216,15 @@
|
||||
<Compile Include="REST\RestResource.cs" />
|
||||
<Compile Include="RootFolders\RootFolderModule.cs" />
|
||||
<Compile Include="RootFolders\RootFolderResource.cs" />
|
||||
<Compile Include="SeasonPass\SeasonPassResource.cs" />
|
||||
<Compile Include="Series\AlternateTitleResource.cs" />
|
||||
<Compile Include="Series\SeasonResource.cs" />
|
||||
<Compile Include="SeasonPass\SeasonPassModule.cs" />
|
||||
<Compile Include="Series\SeriesEditorModule.cs" />
|
||||
<Compile Include="Series\SeriesLookupModule.cs" />
|
||||
<Compile Include="Series\SeriesModule.cs" />
|
||||
<Compile Include="Series\SeriesResource.cs" />
|
||||
<Compile Include="Series\SeasonStatisticsResource.cs" />
|
||||
<Compile Include="System\Backup\BackupModule.cs" />
|
||||
<Compile Include="System\Backup\BackupResource.cs" />
|
||||
<Compile Include="System\Tasks\TaskModule.cs" />
|
||||
@ -272,4 +275,4 @@
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
</Project>
|
33
src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs
Normal file
33
src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using Nancy;
|
||||
using NzbDrone.Api.Extensions;
|
||||
using NzbDrone.Api.Mapping;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Api.SeasonPass
|
||||
{
|
||||
public class SeasonPassModule : NzbDroneApiModule
|
||||
{
|
||||
private readonly IEpisodeMonitoredService _episodeMonitoredService;
|
||||
|
||||
public SeasonPassModule(IEpisodeMonitoredService episodeMonitoredService)
|
||||
: base("/seasonpass")
|
||||
{
|
||||
_episodeMonitoredService = episodeMonitoredService;
|
||||
Post["/"] = series => UpdateAll();
|
||||
}
|
||||
|
||||
private Response UpdateAll()
|
||||
{
|
||||
//Read from request
|
||||
var request = Request.Body.FromJson<SeasonPassResource>();
|
||||
|
||||
foreach (var s in request.Series)
|
||||
{
|
||||
_episodeMonitoredService.SetEpisodeMonitoredStatus(s, request.MonitoringOptions);
|
||||
}
|
||||
|
||||
return "ok".AsResponse(HttpStatusCode.Accepted);
|
||||
}
|
||||
}
|
||||
}
|
11
src/NzbDrone.Api/SeasonPass/SeasonPassResource.cs
Normal file
11
src/NzbDrone.Api/SeasonPass/SeasonPassResource.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Api.SeasonPass
|
||||
{
|
||||
public class SeasonPassResource
|
||||
{
|
||||
public List<Core.Tv.Series> Series { get; set; }
|
||||
public MonitoringOptions MonitoringOptions { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeasonResource
|
||||
{
|
||||
public int SeasonNumber { get; set; }
|
||||
public Boolean Monitored { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public SeasonStatisticsResource Statistics { get; set; }
|
||||
}
|
||||
}
|
||||
|
24
src/NzbDrone.Api/Series/SeasonStatisticsResource.cs
Normal file
24
src/NzbDrone.Api/Series/SeasonStatisticsResource.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeasonStatisticsResource
|
||||
{
|
||||
public DateTime? NextAiring { get; set; }
|
||||
public DateTime? PreviousAiring { get; set; }
|
||||
public int EpisodeFileCount { get; set; }
|
||||
public int EpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
|
||||
public decimal PercentOfEpisodes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (EpisodeCount == 0) return 0;
|
||||
|
||||
return (decimal)EpisodeFileCount / (decimal)EpisodeCount * 100;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -165,6 +165,11 @@ private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seri
|
||||
resource.NextAiring = seriesStatistics.NextAiring;
|
||||
resource.PreviousAiring = seriesStatistics.PreviousAiring;
|
||||
resource.SizeOnDisk = seriesStatistics.SizeOnDisk;
|
||||
|
||||
foreach (var season in resource.Seasons)
|
||||
{
|
||||
season.Statistics = seriesStatistics.SeasonStatistics.SingleOrDefault(s => s.SeasonNumber == season.SeasonNumber).InjectTo<SeasonStatisticsResource>();
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateAlternateTitles(List<SeriesResource> resources)
|
||||
|
@ -69,6 +69,8 @@ public Int32 SeasonCount
|
||||
public DateTime Added { get; set; }
|
||||
public AddSeriesOptions AddOptions { get; set; }
|
||||
|
||||
//TODO: Add series statistics as a property of the series (instead of individual properties)
|
||||
|
||||
//Used to support legacy consumers
|
||||
public Int32 QualityProfileId
|
||||
{
|
||||
|
@ -338,7 +338,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\EpisodeMonitoredServiceTests\SetEpisodeMontitoredFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesRepositoryTests\SeriesRepositoryFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesServiceTests\AddSeriesFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" />
|
||||
|
@ -9,10 +9,10 @@
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.TvTests.SeriesAddedHandlerTests
|
||||
namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SetEpisodeMontitoredFixture : CoreTest<SeriesScannedHandler>
|
||||
public class SetEpisodeMontitoredFixture : CoreTest<EpisodeMonitoredService>
|
||||
{
|
||||
private Series _series;
|
||||
private List<Episode> _episodes;
|
||||
@ -56,16 +56,6 @@ public void Setup()
|
||||
.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)
|
||||
@ -79,8 +69,7 @@ private void GivenSpecials()
|
||||
[Test]
|
||||
public void should_be_able_to_monitor_all_episodes()
|
||||
{
|
||||
WithSeriesAddedEvent(new AddSeriesOptions());
|
||||
TriggerSeriesScannedEvent();
|
||||
Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions());
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.UpdateEpisodes(It.Is<List<Episode>>(l => l.All(e => e.Monitored))));
|
||||
@ -89,13 +78,13 @@ public void should_be_able_to_monitor_all_episodes()
|
||||
[Test]
|
||||
public void should_be_able_to_monitor_missing_episodes_only()
|
||||
{
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = false
|
||||
});
|
||||
var monitoringOptions = new MonitoringOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = false
|
||||
};
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions);
|
||||
|
||||
VerifyMonitored(e => !e.HasFile);
|
||||
VerifyNotMonitored(e => e.HasFile);
|
||||
@ -104,13 +93,13 @@ public void should_be_able_to_monitor_missing_episodes_only()
|
||||
[Test]
|
||||
public void should_be_able_to_monitor_new_episodes_only()
|
||||
{
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
var monitoringOptions = new MonitoringOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = true
|
||||
});
|
||||
};
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions);
|
||||
|
||||
VerifyMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.After(DateTime.UtcNow));
|
||||
VerifyMonitored(e => !e.AirDateUtc.HasValue);
|
||||
@ -122,13 +111,13 @@ public void should_not_monitor_missing_specials()
|
||||
{
|
||||
GivenSpecials();
|
||||
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
var monitoringOptions = new MonitoringOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = false
|
||||
});
|
||||
};
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions);
|
||||
|
||||
VerifyMonitored(e => !e.HasFile);
|
||||
VerifyNotMonitored(e => e.HasFile);
|
||||
@ -139,13 +128,14 @@ public void should_not_monitor_new_specials()
|
||||
{
|
||||
GivenSpecials();
|
||||
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
var monitoringOptions = new MonitoringOptions
|
||||
{
|
||||
IgnoreEpisodesWithFiles = true,
|
||||
IgnoreEpisodesWithoutFiles = true
|
||||
});
|
||||
};
|
||||
|
||||
Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions);
|
||||
|
||||
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));
|
||||
@ -174,17 +164,28 @@ public void should_not_monitor_season_when_all_episodes_are_monitored_except_lat
|
||||
.Setup(s => s.GetEpisodeBySeries(It.IsAny<int>()))
|
||||
.Returns(_episodes);
|
||||
|
||||
WithSeriesAddedEvent(new AddSeriesOptions
|
||||
var monitoringOptions = new MonitoringOptions
|
||||
{
|
||||
IgnoreEpisodesWithoutFiles = true
|
||||
});
|
||||
|
||||
TriggerSeriesScannedEvent();
|
||||
};
|
||||
|
||||
Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions);
|
||||
|
||||
VerifySeasonMonitored(n => n.SeasonNumber == 2);
|
||||
VerifySeasonNotMonitored(n => n.SeasonNumber == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_ignore_episodes_when_season_is_not_monitored()
|
||||
{
|
||||
_series.Seasons.ForEach(s => s.Monitored = false);
|
||||
|
||||
Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions());
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.UpdateEpisodes(It.Is<List<Episode>>(l => l.All(e => !e.Monitored))));
|
||||
}
|
||||
|
||||
private void VerifyMonitored(Func<Episode, bool> predicate)
|
||||
{
|
||||
Mocker.GetMock<IEpisodeService>()
|
@ -91,7 +91,7 @@ public static void Map()
|
||||
Mapper.Entity<Profile>().RegisterModel("Profiles");
|
||||
Mapper.Entity<Log>().RegisterModel("Logs");
|
||||
Mapper.Entity<NamingConfig>().RegisterModel("NamingConfig");
|
||||
Mapper.Entity<SeriesStatistics>().MapResultSet();
|
||||
Mapper.Entity<SeasonStatistics>().MapResultSet();
|
||||
Mapper.Entity<Blacklist>().RegisterModel("Blacklist");
|
||||
Mapper.Entity<MetadataFile>().RegisterModel("MetadataFiles");
|
||||
|
||||
|
@ -869,6 +869,7 @@
|
||||
</Compile>
|
||||
<Compile Include="RootFolders\UnmappedFolder.cs" />
|
||||
<Compile Include="Security.cs" />
|
||||
<Compile Include="SeriesStats\SeasonStatistics.cs" />
|
||||
<Compile Include="SeriesStats\SeriesStatistics.cs" />
|
||||
<Compile Include="SeriesStats\SeriesStatisticsRepository.cs" />
|
||||
<Compile Include="SeriesStats\SeriesStatisticsService.cs" />
|
||||
@ -892,6 +893,7 @@
|
||||
<Compile Include="Tv\Commands\RefreshSeriesCommand.cs" />
|
||||
<Compile Include="Tv\Episode.cs" />
|
||||
<Compile Include="Tv\EpisodeCutoffService.cs" />
|
||||
<Compile Include="Tv\EpisodeMonitoredService.cs" />
|
||||
<Compile Include="Tv\EpisodeRepository.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
@ -903,6 +905,7 @@
|
||||
<Compile Include="Tv\Events\SeriesMovedEvent.cs" />
|
||||
<Compile Include="Tv\Events\SeriesRefreshStartingEvent.cs" />
|
||||
<Compile Include="Tv\Events\SeriesUpdatedEvent.cs" />
|
||||
<Compile Include="Tv\MonitoringOptions.cs" />
|
||||
<Compile Include="Tv\MoveSeriesService.cs" />
|
||||
<Compile Include="Tv\Ratings.cs" />
|
||||
<Compile Include="Tv\RefreshEpisodeService.cs" />
|
||||
|
40
src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs
Normal file
40
src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
public class SeasonStatistics : ResultSet
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public int SeasonNumber { get; set; }
|
||||
public string NextAiringString { get; set; }
|
||||
public string PreviousAiringString { get; set; }
|
||||
public int EpisodeFileCount { get; set; }
|
||||
public int EpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
|
||||
public DateTime? NextAiring
|
||||
{
|
||||
get
|
||||
{
|
||||
DateTime nextAiring;
|
||||
|
||||
if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null;
|
||||
|
||||
return nextAiring;
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? PreviousAiring
|
||||
{
|
||||
get
|
||||
{
|
||||
DateTime previousAiring;
|
||||
|
||||
if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null;
|
||||
|
||||
return previousAiring;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
public class SeriesStatistics : ResultSet
|
||||
{
|
||||
public Int32 SeriesId { get; set; }
|
||||
public String NextAiringString { get; set; }
|
||||
public String PreviousAiringString { get; set; }
|
||||
public Int32 EpisodeFileCount { get; set; }
|
||||
public Int32 EpisodeCount { get; set; }
|
||||
public Int64 SizeOnDisk { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public string NextAiringString { get; set; }
|
||||
public string PreviousAiringString { get; set; }
|
||||
public int EpisodeFileCount { get; set; }
|
||||
public int EpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
public List<SeasonStatistics> SeasonStatistics { get; set; }
|
||||
|
||||
public DateTime? NextAiring
|
||||
{
|
||||
|
@ -7,8 +7,8 @@ namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
public interface ISeriesStatisticsRepository
|
||||
{
|
||||
List<SeriesStatistics> SeriesStatistics();
|
||||
SeriesStatistics SeriesStatistics(Int32 seriesId);
|
||||
List<SeasonStatistics> SeriesStatistics();
|
||||
SeasonStatistics SeriesStatistics(Int32 seriesId);
|
||||
}
|
||||
|
||||
public class SeriesStatisticsRepository : ISeriesStatisticsRepository
|
||||
@ -20,7 +20,7 @@ public SeriesStatisticsRepository(IMainDatabase database)
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public List<SeriesStatistics> SeriesStatistics()
|
||||
public List<SeasonStatistics> SeriesStatistics()
|
||||
{
|
||||
var mapper = _database.GetDataMapper();
|
||||
|
||||
@ -32,10 +32,10 @@ public List<SeriesStatistics> SeriesStatistics()
|
||||
sb.AppendLine(GetGroupByClause());
|
||||
var queryText = sb.ToString();
|
||||
|
||||
return mapper.Query<SeriesStatistics>(queryText);
|
||||
return mapper.Query<SeasonStatistics>(queryText);
|
||||
}
|
||||
|
||||
public SeriesStatistics SeriesStatistics(Int32 seriesId)
|
||||
public SeasonStatistics SeriesStatistics(Int32 seriesId)
|
||||
{
|
||||
var mapper = _database.GetDataMapper();
|
||||
|
||||
@ -49,7 +49,7 @@ public SeriesStatistics SeriesStatistics(Int32 seriesId)
|
||||
sb.AppendLine(GetGroupByClause());
|
||||
var queryText = sb.ToString();
|
||||
|
||||
return mapper.Find<SeriesStatistics>(queryText);
|
||||
return mapper.Find<SeasonStatistics>(queryText);
|
||||
}
|
||||
|
||||
private String GetSelectClause()
|
||||
@ -57,17 +57,18 @@ private String GetSelectClause()
|
||||
return @"SELECT Episodes.*, SUM(EpisodeFiles.Size) as SizeOnDisk FROM
|
||||
(SELECT
|
||||
Episodes.SeriesId,
|
||||
Episodes.SeasonNumber,
|
||||
SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount,
|
||||
SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount,
|
||||
MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString,
|
||||
MAX(CASE WHEN AirDateUtc >= @currentDate OR EpisodeFileId = 0 AND Monitored = 0 THEN NULL ELSE AirDateUtc END) AS PreviousAiringString
|
||||
FROM Episodes
|
||||
GROUP BY Episodes.SeriesId) as Episodes";
|
||||
GROUP BY Episodes.SeriesId, Episodes.SeasonNumber) as Episodes";
|
||||
}
|
||||
|
||||
private String GetGroupByClause()
|
||||
{
|
||||
return "GROUP BY Episodes.SeriesId";
|
||||
return "GROUP BY Episodes.SeriesId, Episodes.SeasonNumber";
|
||||
}
|
||||
|
||||
private String GetEpisodeFilesJoin()
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
@ -19,7 +21,9 @@ public SeriesStatisticsService(ISeriesStatisticsRepository seriesStatisticsRepos
|
||||
|
||||
public List<SeriesStatistics> SeriesStatistics()
|
||||
{
|
||||
return _seriesStatisticsRepository.SeriesStatistics();
|
||||
var seasonStatistics = _seriesStatisticsRepository.SeriesStatistics();
|
||||
|
||||
return seasonStatistics.GroupBy(s => s.SeriesId).Select(s => MapSeriesStatistics(s.ToList())).ToList();
|
||||
}
|
||||
|
||||
public SeriesStatistics SeriesStatistics(int seriesId)
|
||||
@ -28,7 +32,36 @@ public SeriesStatistics SeriesStatistics(int seriesId)
|
||||
|
||||
if (stats == null) return new SeriesStatistics();
|
||||
|
||||
return stats;
|
||||
return MapSeriesStatistics(new List<SeasonStatistics> { stats });
|
||||
}
|
||||
|
||||
private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics)
|
||||
{
|
||||
return new SeriesStatistics
|
||||
{
|
||||
SeasonStatistics = seasonStatistics,
|
||||
SeriesId = seasonStatistics.First().SeriesId,
|
||||
EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount),
|
||||
EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount),
|
||||
SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk),
|
||||
NextAiringString = seasonStatistics.OrderBy(s =>
|
||||
{
|
||||
DateTime nextAiring;
|
||||
|
||||
if (!DateTime.TryParse(s.NextAiringString, out nextAiring)) return DateTime.MinValue;
|
||||
|
||||
return nextAiring;
|
||||
}).First().NextAiringString,
|
||||
|
||||
PreviousAiringString = seasonStatistics.OrderBy(s =>
|
||||
{
|
||||
DateTime nextAiring;
|
||||
|
||||
if (!DateTime.TryParse(s.PreviousAiringString, out nextAiring)) return DateTime.MinValue;
|
||||
|
||||
return nextAiring;
|
||||
}).Last().PreviousAiringString
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,7 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class AddSeriesOptions : IEmbeddedDocument
|
||||
public class AddSeriesOptions : MonitoringOptions
|
||||
{
|
||||
public bool SearchForMissingEpisodes { get; set; }
|
||||
public bool IgnoreEpisodesWithFiles { get; set; }
|
||||
public bool IgnoreEpisodesWithoutFiles { get; set; }
|
||||
}
|
||||
}
|
||||
|
100
src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs
Normal file
100
src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public interface IEpisodeMonitoredService
|
||||
{
|
||||
void SetEpisodeMonitoredStatus(Series series, MonitoringOptions monitoringOptions);
|
||||
}
|
||||
|
||||
public class EpisodeMonitoredService : IEpisodeMonitoredService
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public EpisodeMonitoredService(ISeriesService seriesService, IEpisodeService episodeService, Logger logger)
|
||||
{
|
||||
_seriesService = seriesService;
|
||||
_episodeService = episodeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void SetEpisodeMonitoredStatus(Series series, MonitoringOptions monitoringOptions)
|
||||
{
|
||||
_logger.Debug("[{0}] Setting episode monitored status.", series.Title);
|
||||
|
||||
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
|
||||
if (monitoringOptions.IgnoreEpisodesWithFiles)
|
||||
{
|
||||
_logger.Debug("Ignoring Episodes with Files");
|
||||
ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), false);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_logger.Debug("Monitoring Episodes with Files");
|
||||
ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), true);
|
||||
}
|
||||
|
||||
if (monitoringOptions.IgnoreEpisodesWithoutFiles)
|
||||
{
|
||||
_logger.Debug("Ignoring Episodes without Files");
|
||||
ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), false);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_logger.Debug("Monitoring Episodes without Files");
|
||||
ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), true);
|
||||
}
|
||||
|
||||
var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault();
|
||||
|
||||
foreach (var s in series.Seasons)
|
||||
{
|
||||
var season = s;
|
||||
|
||||
if (season.Monitored)
|
||||
{
|
||||
if (!monitoringOptions.IgnoreEpisodesWithFiles && !monitoringOptions.IgnoreEpisodesWithoutFiles)
|
||||
{
|
||||
ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), true);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (!monitoringOptions.IgnoreEpisodesWithFiles && !monitoringOptions.IgnoreEpisodesWithoutFiles)
|
||||
{
|
||||
ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), false);
|
||||
}
|
||||
}
|
||||
|
||||
if (season.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 ToggleEpisodesMonitoredState(IEnumerable<Episode> episodes, bool monitored)
|
||||
{
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
episode.Monitored = monitored;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
src/NzbDrone.Core/Tv/MonitoringOptions.cs
Normal file
10
src/NzbDrone.Core/Tv/MonitoringOptions.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class MonitoringOptions : IEmbeddedDocument
|
||||
{
|
||||
public bool IgnoreEpisodesWithFiles { get; set; }
|
||||
public bool IgnoreEpisodesWithoutFiles { get; set; }
|
||||
}
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NLog;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
@ -13,61 +9,23 @@ namespace NzbDrone.Core.Tv
|
||||
public class SeriesScannedHandler : IHandle<SeriesScannedEvent>,
|
||||
IHandle<SeriesScanSkippedEvent>
|
||||
{
|
||||
private readonly IEpisodeMonitoredService _episodeMonitoredService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SeriesScannedHandler(ISeriesService seriesService,
|
||||
IEpisodeService episodeService,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
public SeriesScannedHandler(IEpisodeMonitoredService episodeMonitoredService,
|
||||
ISeriesService seriesService,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
{
|
||||
_episodeMonitoredService = episodeMonitoredService;
|
||||
_seriesService = seriesService;
|
||||
_episodeService = episodeService;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_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)
|
||||
@ -76,9 +34,7 @@ private void HandleScanEvents(Series series)
|
||||
}
|
||||
|
||||
_logger.Info("[{0}] was recently added, performing post-add actions", series.Title);
|
||||
|
||||
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
SetEpisodeMonitoredStatus(series, episodes);
|
||||
_episodeMonitoredService.SetEpisodeMonitoredStatus(series, series.AddOptions);
|
||||
|
||||
if (series.AddOptions.SearchForMissingEpisodes)
|
||||
{
|
||||
|
@ -20,6 +20,7 @@
|
||||
@import "../Shared/FileBrowser/filebrowser";
|
||||
@import "badges";
|
||||
@import "../ManualImport/manualimport";
|
||||
@import "../SeasonPass/seasonpass";
|
||||
|
||||
.main-region {
|
||||
@media (min-width : @screen-lg-min) {
|
||||
|
128
src/UI/SeasonPass/SeasonPassFooterView.js
Normal file
128
src/UI/SeasonPass/SeasonPassFooterView.js
Normal file
@ -0,0 +1,128 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Marionette = require('marionette');
|
||||
var vent = require('vent');
|
||||
var RootFolders = require('../AddSeries/RootFolders/RootFolderCollection');
|
||||
|
||||
module.exports = Marionette.ItemView.extend({
|
||||
template : 'SeasonPass/SeasonPassFooterViewTemplate',
|
||||
|
||||
ui : {
|
||||
monitor : '.x-monitor',
|
||||
selectedCount : '.x-selected-count',
|
||||
container : '.series-editor-footer',
|
||||
actions : '.x-action',
|
||||
indicator : '.x-indicator',
|
||||
indicatorIcon : '.x-indicator-icon'
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-update' : '_update'
|
||||
},
|
||||
|
||||
initialize : function(options) {
|
||||
this.seriesCollection = options.collection;
|
||||
|
||||
RootFolders.fetch().done(function() {
|
||||
RootFolders.synced = true;
|
||||
});
|
||||
|
||||
this.editorGrid = options.editorGrid;
|
||||
this.listenTo(this.seriesCollection, 'backgrid:selected', this._updateInfo);
|
||||
},
|
||||
|
||||
onRender : function() {
|
||||
this._updateInfo();
|
||||
},
|
||||
|
||||
_update : function() {
|
||||
var self = this;
|
||||
var selected = this.editorGrid.getSelectedModels();
|
||||
var monitoringOptions;
|
||||
|
||||
_.each(selected, function(model) {
|
||||
monitoringOptions = self._getMonitoringOptions(model);
|
||||
|
||||
model.set('addOptions', monitoringOptions);
|
||||
});
|
||||
|
||||
var promise = $.ajax({
|
||||
url : window.NzbDrone.ApiRoot + '/seasonpass',
|
||||
type : 'POST',
|
||||
data : JSON.stringify({
|
||||
series : _.map(selected, function (model) {
|
||||
return model.toJSON();
|
||||
}),
|
||||
monitoringOptions : monitoringOptions
|
||||
})
|
||||
});
|
||||
|
||||
this.ui.indicator.show();
|
||||
|
||||
promise.always(function () {
|
||||
self.ui.indicator.hide();
|
||||
});
|
||||
|
||||
promise.done(function () {
|
||||
self.seriesCollection.trigger('seasonpass:saved');
|
||||
});
|
||||
},
|
||||
|
||||
_updateInfo : function() {
|
||||
var selected = this.editorGrid.getSelectedModels();
|
||||
var selectedCount = selected.length;
|
||||
|
||||
this.ui.selectedCount.html('{0} series selected'.format(selectedCount));
|
||||
|
||||
if (selectedCount === 0) {
|
||||
this.ui.actions.attr('disabled', 'disabled');
|
||||
} else {
|
||||
this.ui.actions.removeAttr('disabled');
|
||||
}
|
||||
},
|
||||
|
||||
_getMonitoringOptions : function(model) {
|
||||
var monitor = this.ui.monitor.val();
|
||||
var lastSeason = _.max(model.get('seasons'), 'seasonNumber');
|
||||
var firstSeason = _.min(_.reject(model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber');
|
||||
|
||||
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') {
|
||||
model.setSeasonPass(lastSeason.seasonNumber);
|
||||
}
|
||||
|
||||
else if (monitor === 'first') {
|
||||
model.setSeasonPass(lastSeason.seasonNumber + 1);
|
||||
model.setSeasonMonitored(firstSeason.seasonNumber);
|
||||
}
|
||||
|
||||
else if (monitor === 'missing') {
|
||||
options.ignoreEpisodesWithFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'existing') {
|
||||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'none') {
|
||||
model.setSeasonPass(lastSeason.seasonNumber + 1);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
});
|
25
src/UI/SeasonPass/SeasonPassFooterViewTemplate.hbs
Normal file
25
src/UI/SeasonPass/SeasonPassFooterViewTemplate.hbs
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="series-editor-footer">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-2">
|
||||
<label>Monitor</label>
|
||||
|
||||
<select class="form-control x-action x-monitor">
|
||||
<option value="all">All</option>
|
||||
<option value="future">Future</option>
|
||||
<option value="missing">Missing</option>
|
||||
<option value="existing">Existing</option>
|
||||
<option value="first">First Season</option>
|
||||
<option value="latest">Latest Season</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3 actions">
|
||||
<label class="x-selected-count">0 series selected</label>
|
||||
<div>
|
||||
<button class="btn btn-primary x-action x-update">Update Selected Series</button>
|
||||
<span class="indicator x-indicator"><i class="icon-sonarr-spinner fa-spin"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,9 +1,15 @@
|
||||
var _ = require('underscore');
|
||||
var vent = require('vent');
|
||||
var Backgrid = require('backgrid');
|
||||
var Marionette = require('marionette');
|
||||
var EmptyView = require('../Series/Index/EmptyView');
|
||||
var SeriesCollection = require('../Series/SeriesCollection');
|
||||
var SeasonCollection = require('../Series/SeasonCollection');
|
||||
var SeriesCollectionView = require('./SeriesCollectionView');
|
||||
var LoadingView = require('../Shared/LoadingView');
|
||||
var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout');
|
||||
var FooterView = require('./SeasonPassFooterView');
|
||||
var SelectAllCell = require('../Cells/SelectAllCell');
|
||||
var SeriesStatusCell = require('../Cells/SeriesStatusCell');
|
||||
var SeriesTitleCell = require('../Cells/SeriesTitleCell');
|
||||
var SeasonsCell = require('./SeasonsCell');
|
||||
require('../Mixins/backbone.signalr.mixin');
|
||||
|
||||
module.exports = Marionette.Layout.extend({
|
||||
@ -14,11 +20,38 @@ module.exports = Marionette.Layout.extend({
|
||||
series : '#x-series'
|
||||
},
|
||||
|
||||
columns : [
|
||||
{
|
||||
name : '',
|
||||
cell : SelectAllCell,
|
||||
headerCell : 'select-all',
|
||||
sortable : false
|
||||
},
|
||||
{
|
||||
name : 'statusWeight',
|
||||
label : '',
|
||||
cell : SeriesStatusCell
|
||||
},
|
||||
{
|
||||
name : 'title',
|
||||
label : 'Title',
|
||||
cell : SeriesTitleCell,
|
||||
cellValue : 'this'
|
||||
},
|
||||
{
|
||||
name : 'seasons',
|
||||
label : 'Seasons',
|
||||
cell : SeasonsCell,
|
||||
cellValue : 'this'
|
||||
}
|
||||
],
|
||||
|
||||
initialize : function() {
|
||||
this.seriesCollection = SeriesCollection.clone();
|
||||
this.seriesCollection.shadowCollection.bindSignalR();
|
||||
|
||||
this.listenTo(this.seriesCollection, 'sync', this.render);
|
||||
// this.listenTo(this.seriesCollection, 'sync', this.render);
|
||||
this.listenTo(this.seriesCollection, 'seasonpass:saved', this.render);
|
||||
|
||||
this.filteringOptions = {
|
||||
type : 'radio',
|
||||
@ -59,11 +92,13 @@ module.exports = Marionette.Layout.extend({
|
||||
},
|
||||
|
||||
onRender : function() {
|
||||
this.series.show(new SeriesCollectionView({
|
||||
collection : this.seriesCollection
|
||||
}));
|
||||
|
||||
this._showTable();
|
||||
this._showToolbar();
|
||||
this._showFooter();
|
||||
},
|
||||
|
||||
onClose : function() {
|
||||
vent.trigger(vent.Commands.CloseControlPanelCommand);
|
||||
},
|
||||
|
||||
_showToolbar : function() {
|
||||
@ -73,6 +108,32 @@ module.exports = Marionette.Layout.extend({
|
||||
}));
|
||||
},
|
||||
|
||||
_showTable : function() {
|
||||
if (this.seriesCollection.shadowCollection.length === 0) {
|
||||
this.series.show(new EmptyView());
|
||||
this.toolbar.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.columns[0].sortedCollection = this.seriesCollection;
|
||||
|
||||
this.editorGrid = new Backgrid.Grid({
|
||||
collection : this.seriesCollection,
|
||||
columns : this.columns,
|
||||
className : 'table table-hover'
|
||||
});
|
||||
|
||||
this.series.show(this.editorGrid);
|
||||
this._showFooter();
|
||||
},
|
||||
|
||||
_showFooter : function() {
|
||||
vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({
|
||||
editorGrid : this.editorGrid,
|
||||
collection : this.seriesCollection
|
||||
}));
|
||||
},
|
||||
|
||||
_setFilter : function(buttonContext) {
|
||||
var mode = buttonContext.model.get('key');
|
||||
|
||||
|
26
src/UI/SeasonPass/SeasonsCell.js
Normal file
26
src/UI/SeasonPass/SeasonsCell.js
Normal file
@ -0,0 +1,26 @@
|
||||
var _ = require('underscore');
|
||||
var TemplatedCell = require('../Cells/TemplatedCell');
|
||||
//require('../Handlebars/Helpers/Numbers');
|
||||
|
||||
module.exports = TemplatedCell.extend({
|
||||
className : 'seasons-cell',
|
||||
template : 'SeasonPass/SeasonsCellTemplate',
|
||||
|
||||
events : {
|
||||
'click .x-season-monitored' : '_toggleSeasonMonitored'
|
||||
},
|
||||
|
||||
_toggleSeasonMonitored : function(e) {
|
||||
var target = this.$(e.target).closest('.x-season-monitored');
|
||||
var seasonNumber = parseInt(this.$(target).data('season-number'), 10);
|
||||
var icon = this.$(target).children('.x-season-monitored-icon');
|
||||
|
||||
this.model.setSeasonMonitored(seasonNumber);
|
||||
|
||||
//TODO: unbounce the save so we don't multiple to the server at the same time
|
||||
var savePromise = this.model.save();
|
||||
|
||||
icon.spinForPromise(savePromise);
|
||||
savePromise.always(this.render.bind(this));
|
||||
}
|
||||
});
|
30
src/UI/SeasonPass/SeasonsCellTemplate.hbs
Normal file
30
src/UI/SeasonPass/SeasonsCellTemplate.hbs
Normal file
@ -0,0 +1,30 @@
|
||||
{{#each seasons}}
|
||||
<span class="season label label-default">
|
||||
<span>
|
||||
<span class="x-season-monitored season-monitored" title="Toggle season monitored status" data-season-number="{{seasonNumber}}">
|
||||
<i class="x-season-monitored-icon {{#if monitored}}icon-sonarr-monitored{{else}}icon-sonarr-unmonitored{{/if}}"/>
|
||||
</span>
|
||||
</span>
|
||||
{{#if_eq seasonNumber compare="0"}}
|
||||
<span class="season-number">Specials</span>
|
||||
{{else}}
|
||||
<span class="season-number">S{{Pad2 seasonNumber}}</span>
|
||||
{{/if_eq}}
|
||||
|
||||
<!--{{#if_eq statistics.episodeCount compare=0}}-->
|
||||
<!--{{#if monitored}}-->
|
||||
<!--<span class="badge badge-primary season-status" title="No aired episodes"> </span>-->
|
||||
<!--{{else}}-->
|
||||
<!--<span class="badge badge-warning season-status" title="Season is not monitored"> </span>-->
|
||||
<!--{{/if}}-->
|
||||
<!--{{else}}-->
|
||||
<!--{{#with statistics}}-->
|
||||
<!--{{#if_eq percentOfEpisodes compare=100}}-->
|
||||
<!--<span class="badge badge-success season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span>-->
|
||||
<!--{{else}}-->
|
||||
<!--<span class="badge badge-danger season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span>-->
|
||||
<!--{{/if_eq}}-->
|
||||
<!--{{/with}}-->
|
||||
<!--{{/if_eq}}-->
|
||||
</span>
|
||||
{{/each}}
|
@ -1,6 +0,0 @@
|
||||
var Marionette = require('marionette');
|
||||
var SeriesLayout = require('./SeriesLayout');
|
||||
|
||||
module.exports = Marionette.CollectionView.extend({
|
||||
itemView : SeriesLayout
|
||||
});
|
@ -1,152 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
var Marionette = require('marionette');
|
||||
var Backgrid = require('backgrid');
|
||||
var SeasonCollection = require('../Series/SeasonCollection');
|
||||
|
||||
module.exports = Marionette.Layout.extend({
|
||||
template : 'SeasonPass/SeriesLayoutTemplate',
|
||||
|
||||
ui : {
|
||||
seasonSelect : '.x-season-select',
|
||||
expander : '.x-expander',
|
||||
seasonGrid : '.x-season-grid',
|
||||
seriesMonitored : '.x-series-monitored'
|
||||
},
|
||||
|
||||
events : {
|
||||
'change .x-season-select' : '_seasonSelected',
|
||||
'click .x-expander' : '_expand',
|
||||
'click .x-latest' : '_latest',
|
||||
'click .x-all' : '_all',
|
||||
'click .x-monitored' : '_toggleSeasonMonitored',
|
||||
'click .x-series-monitored' : '_toggleSeriesMonitored'
|
||||
},
|
||||
|
||||
regions : {
|
||||
seasonGrid : '.x-season-grid'
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
this.listenTo(this.model, 'sync', this._setSeriesMonitoredState);
|
||||
this.seasonCollection = new SeasonCollection(this.model.get('seasons'));
|
||||
this.expanded = false;
|
||||
},
|
||||
|
||||
onRender : function() {
|
||||
if (!this.expanded) {
|
||||
this.ui.seasonGrid.hide();
|
||||
}
|
||||
|
||||
this._setExpanderIcon();
|
||||
this._setSeriesMonitoredState();
|
||||
},
|
||||
|
||||
_seasonSelected : function() {
|
||||
var seasonNumber = parseInt(this.ui.seasonSelect.val(), 10);
|
||||
|
||||
if (seasonNumber === -1 || isNaN(seasonNumber)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setSeasonMonitored(seasonNumber);
|
||||
},
|
||||
|
||||
_expand : function() {
|
||||
if (this.expanded) {
|
||||
this.ui.seasonGrid.slideUp();
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.seasonGrid.slideDown();
|
||||
this.expanded = true;
|
||||
}
|
||||
|
||||
this._setExpanderIcon();
|
||||
},
|
||||
|
||||
_setExpanderIcon : function() {
|
||||
if (this.expanded) {
|
||||
this.ui.expander.removeClass('icon-sonarr-expand');
|
||||
this.ui.expander.addClass('icon-sonarr-expanded');
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.expander.removeClass('icon-sonarr-expanded');
|
||||
this.ui.expander.addClass('icon-sonarr-expand');
|
||||
}
|
||||
},
|
||||
|
||||
_latest : function() {
|
||||
var season = _.max(this.model.get('seasons'), function(s) {
|
||||
return s.seasonNumber;
|
||||
});
|
||||
|
||||
this._setSeasonMonitored(season.seasonNumber);
|
||||
},
|
||||
|
||||
_all : function() {
|
||||
var minSeasonNotZero = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber');
|
||||
|
||||
this._setSeasonMonitored(minSeasonNotZero.seasonNumber);
|
||||
},
|
||||
|
||||
_setSeasonMonitored : function(seasonNumber) {
|
||||
var self = this;
|
||||
|
||||
this.model.setSeasonPass(seasonNumber);
|
||||
|
||||
var promise = this.model.save();
|
||||
|
||||
promise.done(function(data) {
|
||||
self.seasonCollection = new SeasonCollection(data);
|
||||
self.render();
|
||||
});
|
||||
},
|
||||
|
||||
_toggleSeasonMonitored : function(e) {
|
||||
var seasonNumber = 0;
|
||||
var element;
|
||||
|
||||
if (e.target.localName === 'i') {
|
||||
seasonNumber = parseInt(this.$(e.target).parent('td').attr('data-season-number'), 10);
|
||||
element = this.$(e.target);
|
||||
}
|
||||
|
||||
else {
|
||||
seasonNumber = parseInt(this.$(e.target).attr('data-season-number'), 10);
|
||||
element = this.$(e.target).children('i');
|
||||
}
|
||||
|
||||
this.model.setSeasonMonitored(seasonNumber);
|
||||
|
||||
var savePromise = this.model.save().always(this.render.bind(this));
|
||||
element.spinForPromise(savePromise);
|
||||
},
|
||||
|
||||
_afterToggleSeasonMonitored : function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
_setSeriesMonitoredState : function() {
|
||||
var monitored = this.model.get('monitored');
|
||||
|
||||
this.ui.seriesMonitored.removeAttr('data-idle-icon');
|
||||
|
||||
if (monitored) {
|
||||
this.ui.seriesMonitored.addClass('icon-sonarr-monitored');
|
||||
this.ui.seriesMonitored.removeClass('icon-sonarr-unmonitored');
|
||||
} else {
|
||||
this.ui.seriesMonitored.addClass('icon-sonarr-unmonitored');
|
||||
this.ui.seriesMonitored.removeClass('icon-sonarr-monitored');
|
||||
}
|
||||
},
|
||||
|
||||
_toggleSeriesMonitored : function() {
|
||||
var savePromise = this.model.save('monitored', !this.model.get('monitored'), {
|
||||
wait : true
|
||||
});
|
||||
|
||||
this.ui.seriesMonitored.spinForPromise(savePromise);
|
||||
}
|
||||
});
|
@ -1,75 +0,0 @@
|
||||
<div class="seasonpass-series">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<i class="icon-sonarr-expand x-expander expander pull-left"/>
|
||||
<i class="x-series-monitored series-monitor-toggle pull-left" title="Toggle monitored state for entire series"/>
|
||||
<div class="title col-md-5">
|
||||
<a href="{{route}}">
|
||||
{{title}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<select class="form-control x-season-select season-select">
|
||||
<option value="-1">Select season...</option>
|
||||
{{#each seasons}}
|
||||
{{#if_eq seasonNumber compare="0"}}
|
||||
<option value="{{seasonNumber}}">Specials</option>
|
||||
{{else}}
|
||||
<option value="{{seasonNumber}}">Season {{seasonNumber}}</option>
|
||||
{{/if_eq}}
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">
|
||||
<span class="help-inline">
|
||||
<i class="icon-sonarr-form-info" title="Selecting a season will unmonitor all previous seasons"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="season-pass-button">
|
||||
<button class="btn x-latest last">Latest Season Only</button>
|
||||
</span>
|
||||
|
||||
<span class="season-pass-button">
|
||||
<button class="btn x-all">All Seasons</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-11">
|
||||
<div class="x-season-grid season-grid">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="sortable">Season</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each seasons}}
|
||||
<tr>
|
||||
<td class="toggle-cell x-monitored" data-season-number="{{seasonNumber}}">
|
||||
{{#if monitored}}
|
||||
<i class="icon-sonarr-monitored"></i>
|
||||
{{else}}
|
||||
<i class="icon-sonarr-unmonitored"></i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if_eq seasonNumber compare="0"}}
|
||||
Specials
|
||||
{{else}}
|
||||
Season {{seasonNumber}}
|
||||
{{/if_eq}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
21
src/UI/SeasonPass/seasonpass.less
Normal file
21
src/UI/SeasonPass/seasonpass.less
Normal file
@ -0,0 +1,21 @@
|
||||
@import "../Shared/Styles/clickable.less";
|
||||
|
||||
.season {
|
||||
display : inline-block;
|
||||
font-size : 14px;
|
||||
margin-bottom : 4px;
|
||||
background-color: #eee;
|
||||
border: 1px solid #999;
|
||||
color: #999;
|
||||
|
||||
.season-monitored {
|
||||
display : inline-block;
|
||||
width : 16px;
|
||||
|
||||
.clickable();
|
||||
}
|
||||
|
||||
.season-number {
|
||||
font-size : 12px;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user