Merge branch 'season-start' into develop

pull/4/head
Mark McDowall 11 years ago
commit 4025d6ddee

@ -140,8 +140,6 @@
<Compile Include="RootFolders\RootFolderModule.cs" />
<Compile Include="RootFolders\RootFolderResource.cs" />
<Compile Include="RootFolders\RootFolderConnection.cs" />
<Compile Include="Seasons\SeasonModule.cs" />
<Compile Include="Seasons\SeasonResource.cs" />
<Compile Include="Series\SeriesConnection.cs" />
<Compile Include="Series\SeriesResource.cs" />
<Compile Include="Series\SeriesModule.cs" />

@ -1,53 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Api.Mapping;
using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Seasons
{
public class SeasonModule : NzbDroneRestModule<SeasonResource>
{
private readonly ISeasonService _seasonService;
public SeasonModule(ISeasonService seasonService)
: base("/season")
{
_seasonService = seasonService;
GetResourceAll = GetSeasons;
GetResourceById = GetSeason;
UpdateResource = Update;
Post["/pass"] = x => SetSeasonPass();
}
private List<SeasonResource> GetSeasons()
{
var seriesId = Request.Query.SeriesId;
if (seriesId.HasValue)
{
return ToListResource<Season>(() => _seasonService.GetSeasonsBySeries(seriesId));
}
return ToListResource(() => _seasonService.GetAllSeasons());
}
private SeasonResource GetSeason(int id)
{
return _seasonService.Get(id).InjectTo<SeasonResource>();
}
private void Update(SeasonResource seasonResource)
{
_seasonService.SetMonitored(seasonResource.SeriesId, seasonResource.SeasonNumber, seasonResource.Monitored);
}
private List<SeasonResource> SetSeasonPass()
{
var seriesId = Request.Form.SeriesId;
var seasonNumber = Request.Form.SeasonNumber;
return ToListResource<Season>(() => _seasonService.SetSeasonPass(seriesId, seasonNumber));
}
}
}

@ -1,12 +0,0 @@
using System;
using NzbDrone.Api.REST;
namespace NzbDrone.Api.Seasons
{
public class SeasonResource : RestResource
{
public int SeriesId { get; set; }
public int SeasonNumber { get; set; }
public Boolean Monitored { get; set; }
}
}

@ -117,7 +117,6 @@ namespace NzbDrone.Api.Series
{
resource.EpisodeCount = seriesStatistics.EpisodeCount;
resource.EpisodeFileCount = seriesStatistics.EpisodeFileCount;
resource.SeasonCount = seriesStatistics.SeasonCount;
resource.NextAiring = seriesStatistics.NextAiring;
}
}

@ -14,7 +14,17 @@ namespace NzbDrone.Api.Series
//View Only
public String Title { get; set; }
public Int32 SeasonCount { get; set; }
public Int32 SeasonCount
{
get
{
if (Seasons != null) return Seasons.Count;
return 0;
}
}
public Int32 EpisodeCount { get; set; }
public Int32 EpisodeFileCount { get; set; }
public SeriesStatusType Status { get; set; }
@ -26,7 +36,8 @@ namespace NzbDrone.Api.Series
public List<MediaCover> Images { get; set; }
public String RemotePoster { get; set; }
public List<Season> Seasons { get; set; }
public Int32 Year { get; set; }
//View & Edit
public String Path { get; set; }

@ -177,7 +177,6 @@
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesRepositoryReadFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithoutFilesFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesBetweenDatesFixture.cs" />
<Compile Include="TvTests\SeasonProviderTest.cs" />
<Compile Include="DecisionEngineTests\RetentionSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\QualityAllowedByProfileSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\UpgradeHistorySpecificationFixture.cs" />
@ -188,9 +187,6 @@
<Compile Include="ProviderTests\DiskProviderTests\ArchiveProviderFixture.cs" />
<Compile Include="MediaFiles\DownloadedEpisodesImportServiceFixture.cs" />
<Compile Include="SeriesStatsTests\SeriesStatisticsFixture.cs" />
<Compile Include="TvTests\SeasonServiceTests\HandleEpisodeInfoDeletedEventFixture.cs" />
<Compile Include="TvTests\SeasonServiceTests\SetSeasonPassFixture.cs" />
<Compile Include="TvTests\SeasonServiceTests\SetMonitoredFixture.cs" />
<Compile Include="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" />
<Compile Include="UpdateTests\UpdateServiceFixture.cs" />
<Compile Include="ProviderTests\XemCommunicationProviderTests\GetSceneTvdbMappingsFixture.cs" />

@ -1,57 +0,0 @@
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.TvTests
{
public class SeasonProviderTest : DbTest<SeasonRepository, Season>
{
[TestCase(true)]
[TestCase(false)]
public void Ismonitored_should_return_monitored_status_of_season(bool monitored)
{
var fakeSeason = Builder<Season>.CreateNew()
.With(s => s.Monitored = monitored)
.BuildNew<Season>();
Db.Insert(fakeSeason);
var result = Subject.IsMonitored(fakeSeason.SeriesId, fakeSeason.SeasonNumber);
result.Should().Be(monitored);
}
[Test]
public void Monitored_should_return_true_if_not_in_db()
{
Subject.IsMonitored(10, 0).Should().BeTrue();
}
[Test]
public void GetSeason_should_return_seasons_for_specified_series_only()
{
var seriesA = new[] { 1, 2, 3 };
var seriesB = new[] { 4, 5, 6 };
var seasonsA = seriesA.Select(c => new Season {SeasonNumber = c, SeriesId = 1}).ToList();
var seasonsB = seriesB.Select(c => new Season {SeasonNumber = c, SeriesId = 2}).ToList();
Subject.InsertMany(seasonsA);
Subject.InsertMany(seasonsB);
Subject.GetSeasonNumbers(1).Should().Equal(seriesA);
Subject.GetSeasonNumbers(2).Should().Equal(seriesB);
}
[Test]
public void GetSeason_should_return_emptylist_if_series_doesnt_exist()
{
Subject.GetSeasonNumbers(1).Should().BeEmpty();
}
}
}

@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Test.TvTests.SeasonServiceTests
{
[TestFixture]
public class HandleEpisodeInfoDeletedEventFixture : CoreTest<SeasonService>
{
private List<Season> _seasons;
private List<Episode> _episodes;
[SetUp]
public void Setup()
{
_seasons = Builder<Season>
.CreateListOfSize(1)
.All()
.With(s => s.SeriesId = 1)
.Build()
.ToList();
_episodes = Builder<Episode>
.CreateListOfSize(1)
.All()
.With(e => e.SeasonNumber = _seasons.First().SeasonNumber)
.With(s => s.SeriesId = _seasons.First().SeasonNumber)
.Build()
.ToList();
Mocker.GetMock<ISeasonRepository>()
.Setup(s => s.GetSeasonBySeries(It.IsAny<int>()))
.Returns(_seasons);
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodesBySeason(It.IsAny<int>(), _seasons.First().SeasonNumber))
.Returns(_episodes);
}
private void GivenAbandonedSeason()
{
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodesBySeason(It.IsAny<int>(), _seasons.First().SeasonNumber))
.Returns(new List<Episode>());
}
[Test]
public void should_not_delete_when_season_is_still_valid()
{
Subject.Handle(new EpisodeInfoDeletedEvent(_episodes));
Mocker.GetMock<ISeasonRepository>()
.Verify(v => v.Delete(It.IsAny<Season>()), Times.Never());
}
[Test]
public void should_delete_season_if_no_episodes_exist_in_that_season()
{
GivenAbandonedSeason();
Subject.Handle(new EpisodeInfoDeletedEvent(_episodes));
Mocker.GetMock<ISeasonRepository>()
.Verify(v => v.Delete(It.IsAny<Season>()), Times.Once());
}
[Test]
public void should_only_delete_a_season_once()
{
_episodes = Builder<Episode>
.CreateListOfSize(5)
.All()
.With(e => e.SeasonNumber = _seasons.First().SeasonNumber)
.With(s => s.SeriesId = _seasons.First().SeasonNumber)
.Build()
.ToList();
GivenAbandonedSeason();
Subject.Handle(new EpisodeInfoDeletedEvent(_episodes));
Mocker.GetMock<ISeasonRepository>()
.Verify(v => v.Delete(It.IsAny<Season>()), Times.Once());
}
}
}

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.TvTests.SeasonServiceTests
{
[TestFixture]
public class SetMonitoredFixture : CoreTest<SeasonService>
{
private Season _season;
[SetUp]
public void Setup()
{
_season = new Season
{
Id = 1,
SeasonNumber = 1,
SeriesId = 1,
Monitored = false
};
Mocker.GetMock<ISeasonRepository>()
.Setup(s => s.Get(It.IsAny<Int32>(), It.IsAny<Int32>()))
.Returns(_season);
}
[TestCase(true)]
[TestCase(false)]
public void should_update_season(bool monitored)
{
Subject.SetMonitored(_season.SeriesId, _season.SeasonNumber, monitored);
Mocker.GetMock<ISeasonRepository>()
.Verify(v => v.Update(_season), Times.Once());
}
[TestCase(true)]
[TestCase(false)]
public void should_update_episodes_in_season(bool monitored)
{
Subject.SetMonitored(_season.SeriesId, _season.SeasonNumber, monitored);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.SetEpisodeMonitoredBySeason(_season.SeriesId, _season.SeasonNumber, monitored), Times.Once());
}
}
}

@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.TvTests.SeasonServiceTests
{
[TestFixture]
public class SetSeasonPassFixture : CoreTest<SeasonService>
{
private const Int32 SERIES_ID = 1;
private List<Season> _seasons;
[SetUp]
public void Setup()
{
_seasons = Builder<Season>.CreateListOfSize(5)
.All()
.With(s => s.SeriesId = SERIES_ID)
.Build()
.ToList();
Mocker.GetMock<ISeasonRepository>()
.Setup(s => s.GetSeasonBySeries(It.IsAny<Int32>()))
.Returns(_seasons);
}
[Test]
public void should_updateMany()
{
Subject.SetSeasonPass(SERIES_ID, 1);
Mocker.GetMock<ISeasonRepository>()
.Verify(v => v.UpdateMany(It.IsAny<List<Season>>()), Times.Once());
}
[Test]
public void should_set_lower_seasons_to_false()
{
const int seasonNumber = 3;
var result = Subject.SetSeasonPass(SERIES_ID, seasonNumber);
result.Where(s => s.SeasonNumber < seasonNumber).Should().OnlyContain(s => s.Monitored == false);
}
[Test]
public void should_set_equal_or_higher_seasons_to_false()
{
const int seasonNumber = 3;
var result = Subject.SetSeasonPass(SERIES_ID, seasonNumber);
result.Where(s => s.SeasonNumber >= seasonNumber).Should().OnlyContain(s => s.Monitored == true);
}
[Test]
public void should_set_episodes_in_lower_seasons_to_false()
{
const int seasonNumber = 3;
Subject.SetSeasonPass(SERIES_ID, seasonNumber);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.SetEpisodeMonitoredBySeason(SERIES_ID, It.Is<Int32>(i => i < seasonNumber), false), Times.AtLeastOnce());
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.SetEpisodeMonitoredBySeason(SERIES_ID, It.Is<Int32>(i => i < seasonNumber), true), Times.Never());
}
[Test]
public void should_set_episodes_in_equal_or_higher_seasons_to_false()
{
const int seasonNumber = 3;
Subject.SetSeasonPass(SERIES_ID, seasonNumber);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.SetEpisodeMonitoredBySeason(SERIES_ID, It.Is<Int32>(i => i >= seasonNumber), true), Times.AtLeastOnce());
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.SetEpisodeMonitoredBySeason(SERIES_ID, It.Is<Int32>(i => i >= seasonNumber), false), Times.Never());
}
}
}

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(20)]
public class add_year_and_seasons_to_series : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Series").AddColumn("Year").AsInt32().Nullable();
Alter.Table("Series").AddColumn("Seasons").AsString().Nullable();
Execute.WithConnection(ConvertSeasons);
}
private void ConvertSeasons(IDbConnection conn, IDbTransaction tran)
{
using (IDbCommand allSeriesCmd = conn.CreateCommand())
{
allSeriesCmd.Transaction = tran;
allSeriesCmd.CommandText = @"SELECT Id FROM Series";
using (IDataReader allSeriesReader = allSeriesCmd.ExecuteReader())
{
while (allSeriesReader.Read())
{
int seriesId = allSeriesReader.GetInt32(0);
var seasons = new List<dynamic>();
using (IDbCommand seasonsCmd = conn.CreateCommand())
{
seasonsCmd.Transaction = tran;
seasonsCmd.CommandText = String.Format(@"SELECT SeasonNumber, Monitored FROM Seasons WHERE SeriesId = {0}", seriesId);
using (IDataReader seasonReader = seasonsCmd.ExecuteReader())
{
while (seasonReader.Read())
{
int seasonNumber = seasonReader.GetInt32(0);
bool monitored = seasonReader.GetBoolean(1);
seasons.Add(new { seasonNumber, monitored });
}
}
}
using (IDbCommand updateCmd = conn.CreateCommand())
{
var text = String.Format("UPDATE Series SET Seasons = '{0}' WHERE Id = {1}", seasons.ToJson() , seriesId);
updateCmd.Transaction = tran;
updateCmd.CommandText = text;
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}

@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(21)]
public class drop_seasons_table : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Delete.Table("Seasons");
}
}
}

@ -45,8 +45,6 @@ namespace NzbDrone.Core.Datastore
.Relationship()
.HasOne(s => s.QualityProfile, s => s.QualityProfileId);
Mapper.Entity<Season>().RegisterModel("Seasons");
Mapper.Entity<Episode>().RegisterModel("Episodes")
.Ignore(e => e.SeriesTitle)
.Ignore(e => e.Series)

@ -29,31 +29,4 @@ namespace NzbDrone.Core.MetadataSource.Trakt
public List<string> genres { get; set; }
public List<Season> seasons { get; set; }
}
public class SearchShow
{
public string title { get; set; }
public int year { get; set; }
public string url { get; set; }
public int first_aired { get; set; }
public string first_aired_iso { get; set; }
public int first_aired_utc { get; set; }
public string country { get; set; }
public string overview { get; set; }
public int runtime { get; set; }
public string status { get; set; }
public string network { get; set; }
public string air_day { get; set; }
public string air_day_utc { get; set; }
public string air_time { get; set; }
public string air_time_utc { get; set; }
public string certification { get; set; }
public string imdb_id { get; set; }
public int tvdb_id { get; set; }
public int tvrage_id { get; set; }
public int last_updated { get; set; }
public string poster { get; set; }
public Images images { get; set; }
public List<string> genres { get; set; }
}
}

@ -30,10 +30,10 @@ namespace NzbDrone.Core.MetadataSource
try
{
var client = BuildClient("search", "shows");
var restRequest = new RestRequest(GetSearchTerm(title));
var response = client.ExecuteAndValidate<List<SearchShow>>(restRequest);
var restRequest = new RestRequest(GetSearchTerm(title) +"/30/seasons");
var response = client.ExecuteAndValidate<List<Show>>(restRequest);
return response.Select(MapSearchSeries).ToList();
return response.Select(MapSeries).ToList();
}
catch (WebException ex)
{
@ -71,6 +71,7 @@ namespace NzbDrone.Core.MetadataSource
series.ImdbId = show.imdb_id;
series.Title = show.title;
series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.title);
series.Year = show.year;
series.FirstAired = FromIso(show.first_aired_iso);
series.Overview = show.overview;
series.Runtime = show.runtime;
@ -79,27 +80,10 @@ namespace NzbDrone.Core.MetadataSource
series.TitleSlug = show.url.ToLower().Replace("http://trakt.tv/show/", "");
series.Status = GetSeriesStatus(show.status);
series.Images.Add(new MediaCover.MediaCover { CoverType = MediaCoverTypes.Banner, Url = show.images.banner });
series.Images.Add(new MediaCover.MediaCover { CoverType = MediaCoverTypes.Poster, Url = GetPosterThumbnailUrl(show.images.poster) });
series.Images.Add(new MediaCover.MediaCover { CoverType = MediaCoverTypes.Fanart, Url = show.images.fanart });
return series;
}
private static Series MapSearchSeries(SearchShow show)
{
var series = new Series();
series.TvdbId = show.tvdb_id;
series.TvRageId = show.tvrage_id;
series.ImdbId = show.imdb_id;
series.Title = show.title;
series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.title);
series.FirstAired = FromIso(show.first_aired_iso);
series.Overview = show.overview;
series.Runtime = show.runtime;
series.Network = show.network;
series.AirTime = show.air_time_utc;
series.TitleSlug = show.url.ToLower().Replace("http://trakt.tv/show/", "");
series.Status = GetSeriesStatus(show.status);
series.Seasons = show.seasons.Select(s => new Tv.Season
{
SeasonNumber = s.season
}).OrderByDescending(s => s.SeasonNumber).ToList();
series.Images.Add(new MediaCover.MediaCover { CoverType = MediaCoverTypes.Banner, Url = show.images.banner });
series.Images.Add(new MediaCover.MediaCover { CoverType = MediaCoverTypes.Poster, Url = GetPosterThumbnailUrl(show.images.poster) });

@ -163,6 +163,8 @@
<Compile Include="Datastore\Migration\017_reset_scene_names.cs" />
<Compile Include="Datastore\Migration\018_remove_duplicates.cs" />
<Compile Include="Datastore\Migration\019_restore_unique_constraints.cs" />
<Compile Include="Datastore\Migration\020_add_year_and_seasons_to_series.cs" />
<Compile Include="Datastore\Migration\021_drop_seasons_table.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" />

@ -6,7 +6,6 @@ namespace NzbDrone.Core.SeriesStats
public class SeriesStatistics : ResultSet
{
public int SeriesId { get; set; }
public int SeasonCount { get; set; }
public string NextAiringString { get; set; }
public int EpisodeFileCount { get; set; }
public int EpisodeCount { get; set; }

@ -56,7 +56,6 @@ namespace NzbDrone.Core.SeriesStats
SeriesId,
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,
COUNT(DISTINCT(CASE WHEN SeasonNumber > 0 THEN SeasonNumber ELSE NULL END)) as SeasonCount,
MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString
FROM Episodes";
}

@ -114,11 +114,6 @@ namespace NzbDrone.Core.Tv
private static bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons)
{
if (episode.SeasonNumber == 0)
{
return false;
}
if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1)
{
return false;

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
@ -64,6 +65,8 @@ namespace NzbDrone.Core.Tv
_logger.WarnException("Couldn't update series path for " + series.Path, e);
}
series.Seasons = UpdateSeasons(series, seriesInfo);
_seriesService.UpdateSeries(series);
_refreshEpisodeService.RefreshEpisodeInfo(series, tuple.Item2);
@ -71,6 +74,21 @@ namespace NzbDrone.Core.Tv
_messageAggregator.PublishEvent(new SeriesUpdatedEvent(series));
}
private List<Season> UpdateSeasons(Series series, Series seriesInfo)
{
foreach (var season in seriesInfo.Seasons)
{
var existingSeason = series.Seasons.SingleOrDefault(s => s.SeasonNumber == season.SeasonNumber);
if (existingSeason != null)
{
season.Monitored = existingSeason.Monitored;
}
}
return seriesInfo.Seasons;
}
public void Execute(RefreshSeriesCommand message)
{
if (message.SeriesId.HasValue)

@ -4,12 +4,9 @@ using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Tv
{
public class Season : ModelBase
public class Season : IEmbeddedDocument
{
public int SeriesId { get; set; }
public int SeasonNumber { get; set; }
public Boolean Monitored { get; set; }
public List<Episode> Episodes { get; set; }
}
}

@ -6,44 +6,21 @@ using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Tv
{
public interface ISeasonRepository : IBasicRepository<Season>
public interface ISeasonRepository : IBasicRepository<Series>
{
IList<int> GetSeasonNumbers(int seriesId);
Season Get(int seriesId, int seasonNumber);
bool IsMonitored(int seriesId, int seasonNumber);
List<Season> GetSeasonBySeries(int seriesId);
}
public class SeasonRepository : BasicRepository<Season>, ISeasonRepository
public class SeasonRepository : BasicRepository<Series>, ISeasonRepository
{
public SeasonRepository(IDatabase database, IMessageAggregator messageAggregator)
: base(database, messageAggregator)
{
}
public IList<int> GetSeasonNumbers(int seriesId)
{
return Query.Where(c => c.SeriesId == seriesId).Select(c => c.SeasonNumber).ToList();
}
public Season Get(int seriesId, int seasonNumber)
{
return Query.Single(s => s.SeriesId == seriesId && s.SeasonNumber == seasonNumber);
}
public bool IsMonitored(int seriesId, int seasonNumber)
{
var season = Query.SingleOrDefault(s => s.SeriesId == seriesId && s.SeasonNumber == seasonNumber);
if (season == null) return true;
return season.Monitored;
}
public List<Season> GetSeasonBySeries(int seriesId)
{
return Query.Where(s => s.SeriesId == seriesId);
return Query.Single(s => s.Id == seriesId).Seasons;
}
}
}

Binary file not shown.

@ -34,12 +34,15 @@ namespace NzbDrone.Core.Tv
public bool UseSceneNumbering { get; set; }
public string TitleSlug { get; set; }
public string Path { get; set; }
public int Year { get; set; }
public string RootFolderPath { get; set; }
public DateTime? FirstAired { get; set; }
public LazyLoaded<QualityProfile> QualityProfile { get; set; }
public List<Season> Seasons { get; set; }
public override string ToString()
{
return string.Format("[{0}][{1}]", TvdbId, Title.NullSafe());

@ -37,18 +37,21 @@ namespace NzbDrone.Core.Tv
private readonly IConfigService _configService;
private readonly IMessageAggregator _messageAggregator;
private readonly ISceneMappingService _sceneMappingService;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger;
public SeriesService(ISeriesRepository seriesRepository,
IConfigService configServiceService,
IMessageAggregator messageAggregator,
ISceneMappingService sceneMappingService,
IEpisodeService episodeService,
Logger logger)
{
_seriesRepository = seriesRepository;
_configService = configServiceService;
_messageAggregator = messageAggregator;
_sceneMappingService = sceneMappingService;
_episodeService = episodeService;
_logger = logger;
}
@ -155,6 +158,11 @@ namespace NzbDrone.Core.Tv
public Series UpdateSeries(Series series)
{
foreach (var season in series.Seasons)
{
_episodeService.SetEpisodeMonitoredBySeason(series.Id, season.SeasonNumber, season.Monitored);
}
return _seriesRepository.Update(series);
}

@ -1,22 +0,0 @@
using System.Collections.Generic;
using System.Net;
using NzbDrone.Api.Episodes;
using NzbDrone.Api.Seasons;
using RestSharp;
namespace NzbDrone.Integration.Test.Client
{
public class SeasonClient : ClientBase<SeasonResource>
{
public SeasonClient(IRestClient restClient)
: base(restClient)
{
}
public List<SeasonResource> GetSeasonsInSeries(int seriesId)
{
var request = BuildRequest("?seriesId=" + seriesId.ToString());
return Get<List<SeasonResource>>(request);
}
}
}

@ -27,7 +27,6 @@ namespace NzbDrone.Integration.Test
protected ClientBase<HistoryResource> History;
protected IndexerClient Indexers;
protected EpisodeClient Episodes;
protected SeasonClient Seasons;
protected ClientBase<NamingConfigResource> NamingConfig;
private NzbDroneRunner _runner;
@ -64,7 +63,6 @@ namespace NzbDrone.Integration.Test
History = new ClientBase<HistoryResource>(RestClient);
Indexers = new IndexerClient(RestClient);
Episodes = new EpisodeClient(RestClient);
Seasons = new SeasonClient(RestClient);
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming");
}
@ -75,5 +73,4 @@ namespace NzbDrone.Integration.Test
_runner.KillAll();
}
}
}

@ -94,7 +94,6 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Client\ClientBase.cs" />
<Compile Include="Client\SeasonClient.cs" />
<Compile Include="Client\EpisodeClient.cs" />
<Compile Include="Client\IndexerClient.cs" />
<Compile Include="Client\ReleaseClient.cs" />
@ -102,7 +101,6 @@
<Compile Include="CommandIntegerationTests.cs" />
<Compile Include="HistoryIntegrationTest.cs" />
<Compile Include="NamingConfigTests.cs" />
<Compile Include="SeasonIntegrationTests.cs" />
<Compile Include="EpisodeIntegrationTests.cs" />
<Compile Include="IndexerIntegrationFixture.cs" />
<Compile Include="IntegrationTestDirectoryInfo.cs" />

@ -1,61 +0,0 @@
using System;
using System.Threading;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Api.Series;
using System.Linq;
using NzbDrone.Test.Common;
namespace NzbDrone.Integration.Test
{
[TestFixture]
public class SeasonIntegrationTests : IntegrationTest
{
private SeriesResource GivenSeriesWithEpisodes()
{
var series = Series.Lookup("archer").First();
series.QualityProfileId = 1;
series.Path = @"C:\Test\Archer".AsOsAgnostic();
series = Series.Post(series);
while (true)
{
if (Seasons.GetSeasonsInSeries(series.Id).Count > 0)
{
return series;
}
Thread.Sleep(1000);
}
}
[Test]
public void should_be_able_to_get_all_seasons_in_series()
{
var series = GivenSeriesWithEpisodes();
Seasons.GetSeasonsInSeries(series.Id).Count.Should().BeGreaterThan(0);
}
[Test]
public void should_be_able_to_get_a_single_season()
{
var series = GivenSeriesWithEpisodes();
var seasons = Seasons.GetSeasonsInSeries(series.Id);
Seasons.Get(seasons.First().Id).Should().NotBeNull();
}
[Test]
public void should_be_able_to_set_monitor_status_via_api()
{
var series = GivenSeriesWithEpisodes();
var seasons = Seasons.GetSeasonsInSeries(series.Id);
var updatedSeason = seasons.First();
updatedSeason.Monitored = false;
Seasons.Put(updatedSeason).Monitored.Should().BeFalse();
}
}
}

@ -1,4 +1,4 @@
<select class="span6 x-root-folder">
<select class="span4 x-root-folder">
{{#if this}}
{{#each this}}
<option value="{{id}}">{{path}}</option>

@ -0,0 +1,9 @@
<select class="span2 x-starting-season">
{{#each this}}
{{#if_eq seasonNumber compare="0"}}
<option value="{{seasonNumber}}">Specials</option>
{{else}}
<option value="{{seasonNumber}}">Season {{seasonNumber}}</option>
{{/if_eq}}
{{/each}}
</select>

@ -3,7 +3,6 @@ define(
[
'app',
'marionette',
'Quality/QualityProfileCollection',
'AddSeries/RootFolders/Collection',
'AddSeries/RootFolders/Layout',
@ -15,13 +14,14 @@ define(
return Marionette.ItemView.extend({
template: 'AddSeries/SearchResultTemplate',
template: 'AddSeries/SearchResultViewTemplate',
ui: {
qualityProfile: '.x-quality-profile',
rootFolder : '.x-root-folder',
addButton : '.x-add',
overview : '.x-overview'
overview : '.x-overview',
startingSeason: '.x-starting-season'
},
events: {
@ -57,6 +57,12 @@ define(
this.ui.rootFolder.val(defaultRoot);
}
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?
//works with onShow, but stops working after the first render
this.ui.overview.dotdotdot({
@ -117,15 +123,16 @@ define(
var quality = this.ui.qualityProfile.val();
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
var startingSeason = this.ui.startingSeason.val();
this.model.set('qualityProfileId', quality);
this.model.set('rootFolderPath', rootFolderPath);
this.model.setSeasonPass(startingSeason);
var self = this;
SeriesCollection.add(this.model);
this.model.save().done(function () {
self.close();
icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search');

@ -2,36 +2,34 @@
<div class="row">
<div class="span2">
<a href="{{traktUrl}}" target="_blank">
<img class="new-series-poster" src="{{remotePoster}}"
{{defaultImg}} >
<img class="new-series-poster" src="{{remotePoster}}" {{defaultImg}} >
</a>
</div>
<div class="span9">
<div class="row">
<h2>{{title}}</h2>
<h2>{{titleWithYear}}</h2>
</div>
<div class="row new-series-overview x-overview">
{{overview}}
</div>
<div class="row">
{{#if existing}}
<div class="btn pull-right add-series disabled">
<div class="btn add-series disabled pull-right">
Already Exists
</div>
{{else}}
<div class="btn btn-success x-add pull-right add-series">
Add
<icon class="icon-plus"></icon>
</div>
{{#unless path}}
{{> RootFolderSelectionPartial rootFolders}}
{{/unless}}
<div class='pull-right'>
{{> QualityProfileSelectionPartial qualityProfiles}}
{{> StartingSeasonSelectionPartial seasons}}
{{> QualityProfileSelectionPartial qualityProfiles}}
<div class="span1 btn btn-success x-add add-series pull-right">
Add <i class="icon-plus"></i>
</div>
{{/if}}
</div>
</div>
</div>

@ -113,4 +113,12 @@
.icon-nd-status:before {
.icon(@circle);
}
.icon-nd-monitored:before {
.icon(@bookmark);
}
.icon-nd-unmonitored:before {
.icon(@bookmark-empty);
}

@ -62,4 +62,12 @@ define(
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount))
});
Handlebars.registerHelper('titleWithYear', function () {
if (this.title.endsWith(' ({0})'.format(this.year))) {
return this.title;
}
return '{0} ({1})'.format(this.title, this.year);
});
});

@ -24,16 +24,10 @@ define(
this.series.show(new LoadingView());
this.seriesCollection = SeriesCollection;
this.seasonCollection = new SeasonCollection();
var promise = this.seasonCollection.fetch();
promise.done(function () {
self.series.show(new SeriesCollectionView({
collection: self.seriesCollection,
seasonCollection: self.seasonCollection
}));
});
self.series.show(new SeriesCollectionView({
collection: self.seriesCollection
}));
}
});
});

@ -5,22 +5,6 @@ define(
'SeasonPass/SeriesLayout'
], function (Marionette, SeriesLayout) {
return Marionette.CollectionView.extend({
itemView: SeriesLayout,
initialize: function (options) {
if (!options.seasonCollection) {
throw 'seasonCollection is needed';
}
this.seasonCollection = options.seasonCollection;
},
itemViewOptions: function () {
return {
seasonCollection: this.seasonCollection
};
}
itemView: SeriesLayout
});
});

@ -2,11 +2,9 @@
define(
[
'marionette',
'backgrid',
'Series/SeasonCollection',
'Cells/ToggleCell',
'Shared/Actioneer'
], function (Marionette, Backgrid, SeasonCollection, ToggleCell, Actioneer) {
], function (Marionette, SeasonCollection, Actioneer) {
return Marionette.Layout.extend({
template: 'SeasonPass/SeriesLayoutTemplate',
@ -19,48 +17,22 @@ define(
events: {
'change .x-season-select': '_seasonSelected',
'click .x-expander' : '_expand',
'click .x-latest' : '_latest'
'click .x-latest' : '_latest',
'click .x-monitored' : '_toggleSeasonMonitored'
},
regions: {
seasonGrid: '.x-season-grid'
},
columns:
[
{
name : 'monitored',
label : '',
cell : ToggleCell,
trueClass : 'icon-bookmark',
falseClass: 'icon-bookmark-empty',
tooltip : 'Toggle monitored status',
sortable : false
},
{
name : 'seasonNumber',
label: 'Season',
cell : Backgrid.IntegerCell.extend({
className: 'season-number-cell'
})
}
],
initialize: function (options) {
this.seasonCollection = options.seasonCollection.bySeries(this.model.get('id'));
this.model.set('seasons', this.seasonCollection);
initialize: function () {
this.seasonCollection = new SeasonCollection(this.model.get('seasons'));
this.expanded = false;
},
onRender: function () {
this.seasonGrid.show(new Backgrid.Grid({
columns : this.columns,
collection: this.seasonCollection,
className : 'table table-condensed season-grid span5'
}));
if (!this.expanded) {
this.seasonGrid.$el.hide();
this.ui.seasonGrid.hide();
}
this._setExpanderIcon();
@ -103,33 +75,51 @@ define(
},
_latest: function () {
var season = _.max(this.seasonCollection.models, function (model) {
return model.get('seasonNumber');
var season = _.max(this.model.get('seasons'), function (s) {
return s.seasonNumber;
});
//var seasonNumber = season.get('seasonNumber');
this._setMonitored(season.get('seasonNumber'))
this._setMonitored(season.seasonNumber);
},
_setMonitored: function (seasonNumber) {
//TODO: use Actioneer?
var self = this;
var promise = $.ajax({
url: this.seasonCollection.url + '/pass',
type: 'POST',
data: {
seriesId: this.model.get('id'),
seasonNumber: seasonNumber
}
});
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($(e.target).parent('td').attr('data-season-number'));
element = $(e.target);
}
else {
seasonNumber = parseInt($(e.target).attr('data-season-number'));
element = $(e.target).children('i');
}
this.model.setSeasonMonitored(seasonNumber);
Actioneer.SaveModel({
element: element,
context: this,
always : this._afterToggleSeasonMonitored
});
},
_afterToggleSeasonMonitored: function () {
this.render();
}
});
});

@ -12,12 +12,12 @@
<span class="span3">
<select class="x-season-select season-select">
<option value="-1">Select season...</option>
{{#each seasons.models}}
{{#if_eq attributes.seasonNumber compare="0"}}
<option value="{{attributes.seasonNumber}}">Specials</option>
{{else}}
<option value="{{attributes.seasonNumber}}">Season {{attributes.seasonNumber}}</option>
{{/if_eq}}
{{#each seasons}}
{{#if_eq seasonNumber compare="0"}}
<option value="{{seasonNumber}}">Specials</option>
{{else}}
<option value="{{seasonNumber}}">Season {{seasonNumber}}</option>
{{/if_eq}}
{{/each}}
</select>
@ -36,7 +36,36 @@
<div class="row">
<div class="span11">
<div class="x-season-grid season-grid"></div>
<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-nd-monitored"></i>
{{else}}
<i class="icon-nd-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>

@ -117,8 +117,10 @@ define(
_seasonMonitored: function () {
var name = 'monitored';
this.model.set(name, !this.model.get(name));
this.series.setSeasonMonitored(this.model.get('seasonNumber'));
Actioneer.SaveModel({
model : this.series,
context: this,
element: this.ui.seasonMonitored,
always : this._afterSeasonMonitored

@ -113,12 +113,12 @@ define(
this.ui.monitored.removeClass('icon-spin icon-spinner');
if (this.model.get('monitored')) {
this.ui.monitored.addClass('icon-bookmark');
this.ui.monitored.removeClass('icon-bookmark-empty');
this.ui.monitored.addClass('icon-nd-monitored');
this.ui.monitored.removeClass('icon-nd-unmonitored');
}
else {
this.ui.monitored.addClass('icon-bookmark-empty');
this.ui.monitored.removeClass('icon-bookmark');
this.ui.monitored.addClass('icon-nd-unmonitored');
this.ui.monitored.removeClass('icon-nd-monitored');
}
},
@ -176,11 +176,11 @@ define(
this.seasons.show(new LoadingView());
this.seasonCollection = new SeasonCollection();
this.seasonCollection = new SeasonCollection(this.model.get('seasons'));
this.episodeCollection = new EpisodeCollection({ seriesId: this.model.id });
this.episodeFileCollection = new EpisodeFileCollection({ seriesId: this.model.id });
$.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch(), this.seasonCollection.fetch({data: { seriesId: this.model.id }})).done(function () {
$.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function () {
var seasonCollectionView = new SeasonCollectionView({
collection : self.seasonCollection,
episodeCollection: self.episodeCollection,

@ -5,21 +5,10 @@ define(
'Series/SeasonModel'
], function (Backbone, SeasonModel) {
return Backbone.Collection.extend({
url : window.ApiRoot + '/season',
model: SeasonModel,
comparator: function (season) {
return -season.get('seasonNumber');
},
bySeries: function (series) {
var filtered = this.filter(function (season) {
return season.get('seriesId') === series;
});
var SeasonCollection = require('Series/SeasonCollection');
return new SeasonCollection(filtered);
}
});
});

@ -14,6 +14,25 @@ define(
episodeCount : 0,
isExisting : false,
status : 0
},
setSeasonMonitored: function (seasonNumber) {
_.each(this.get('seasons'), function (season) {
if (season.seasonNumber === seasonNumber) {
season.monitored = !season.monitored;
}
});
},
setSeasonPass: function (seasonNumber) {
_.each(this.get('seasons'), function (season) {
if (season.seasonNumber >= seasonNumber) {
season.monitored = true;
}
else {
season.monitored = false;
}
});
}
});
});

@ -256,6 +256,7 @@
.clickable;
line-height: 30px;
margin-left: 8px;
width: 16px;
}
.season-grid {

@ -33,7 +33,9 @@ define(
this._showStartMessage(options);
this._setSpinnerOnElement(options);
var promise = options.context.model.save();
var model = options.model ? options.model : options.context.model;
var promise = model.save();
this._handlePromise(promise, options);
},

Loading…
Cancel
Save