Seasons are now subdocuments of series

This commit is contained in:
Mark McDowall 2013-09-09 22:22:38 -07:00
parent 0861e5f8c1
commit 33986a9185
40 changed files with 256 additions and 633 deletions

View File

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

View File

@ -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));
}
}
}

View File

@ -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; }
}
}

View File

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

View File

@ -14,7 +14,17 @@ namespace NzbDrone.Api.Series
//View Only //View Only
public String Title { get; set; } 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 EpisodeCount { get; set; }
public Int32 EpisodeFileCount { get; set; } public Int32 EpisodeFileCount { get; set; }
public SeriesStatusType Status { get; set; } public SeriesStatusType Status { get; set; }
@ -26,7 +36,7 @@ namespace NzbDrone.Api.Series
public List<MediaCover> Images { get; set; } public List<MediaCover> Images { get; set; }
public String RemotePoster { get; set; } public String RemotePoster { get; set; }
public List<Season> Seasons { get; set; }
//View & Edit //View & Edit
public String Path { get; set; } public String Path { get; set; }

View File

@ -175,7 +175,6 @@
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesRepositoryReadFixture.cs" /> <Compile Include="TvTests\EpisodeRepositoryTests\EpisodesRepositoryReadFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithoutFilesFixture.cs" /> <Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithoutFilesFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesBetweenDatesFixture.cs" /> <Compile Include="TvTests\EpisodeRepositoryTests\EpisodesBetweenDatesFixture.cs" />
<Compile Include="TvTests\SeasonProviderTest.cs" />
<Compile Include="DecisionEngineTests\RetentionSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\RetentionSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\QualityAllowedByProfileSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\QualityAllowedByProfileSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\UpgradeHistorySpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\UpgradeHistorySpecificationFixture.cs" />
@ -187,9 +186,6 @@
<Compile Include="ProviderTests\DiskProviderTests\ArchiveProviderFixture.cs" /> <Compile Include="ProviderTests\DiskProviderTests\ArchiveProviderFixture.cs" />
<Compile Include="MediaFileTests\DropFolderImportServiceFixture.cs" /> <Compile Include="MediaFileTests\DropFolderImportServiceFixture.cs" />
<Compile Include="SeriesStatsTests\SeriesStatisticsFixture.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="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" />
<Compile Include="UpdateTests\UpdateServiceFixture.cs" /> <Compile Include="UpdateTests\UpdateServiceFixture.cs" />
<Compile Include="ProviderTests\XemCommunicationProviderTests\GetSceneTvdbMappingsFixture.cs" /> <Compile Include="ProviderTests\XemCommunicationProviderTests\GetSceneTvdbMappingsFixture.cs" />

View File

@ -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();
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}
}
}
}
}

View File

@ -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");
}
}
}

View File

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

View File

@ -29,31 +29,4 @@ namespace NzbDrone.Core.MetadataSource.Trakt
public List<string> genres { get; set; } public List<string> genres { get; set; }
public List<Season> seasons { 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; }
}
} }

View File

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

View File

@ -162,6 +162,8 @@
<Compile Include="Datastore\Migration\017_reset_scene_names.cs" /> <Compile Include="Datastore\Migration\017_reset_scene_names.cs" />
<Compile Include="Datastore\Migration\018_remove_duplicates.cs" /> <Compile Include="Datastore\Migration\018_remove_duplicates.cs" />
<Compile Include="Datastore\Migration\019_restore_unique_constraints.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\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" />

View File

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

View File

@ -56,7 +56,6 @@ namespace NzbDrone.Core.SeriesStats
SeriesId, SeriesId,
SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, 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, 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 MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString
FROM Episodes"; FROM Episodes";
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -64,6 +65,8 @@ namespace NzbDrone.Core.Tv
_logger.WarnException("Couldn't update series path for " + series.Path, e); _logger.WarnException("Couldn't update series path for " + series.Path, e);
} }
series.Seasons = UpdateSeasons(series, seriesInfo);
_seriesService.UpdateSeries(series); _seriesService.UpdateSeries(series);
_refreshEpisodeService.RefreshEpisodeInfo(series, tuple.Item2); _refreshEpisodeService.RefreshEpisodeInfo(series, tuple.Item2);
@ -71,6 +74,21 @@ namespace NzbDrone.Core.Tv
_messageAggregator.PublishEvent(new SeriesUpdatedEvent(series)); _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) public void Execute(RefreshSeriesCommand message)
{ {
if (message.SeriesId.HasValue) if (message.SeriesId.HasValue)

View File

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

View File

@ -6,44 +6,35 @@ using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Tv 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); Season Get(int seriesId, int seasonNumber);
bool IsMonitored(int seriesId, int seasonNumber); bool IsMonitored(int seriesId, int seasonNumber);
List<Season> GetSeasonBySeries(int seriesId); List<Season> GetSeasonBySeries(int seriesId);
} }
public class SeasonRepository : BasicRepository<Season>, ISeasonRepository public class SeasonRepository : BasicRepository<Series>, ISeasonRepository
{ {
public SeasonRepository(IDatabase database, IMessageAggregator messageAggregator) public SeasonRepository(IDatabase database, IMessageAggregator messageAggregator)
: base(database, 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) public Season Get(int seriesId, int seasonNumber)
{ {
return Query.Single(s => s.SeriesId == seriesId && s.SeasonNumber == seasonNumber); var series = Query.Single(s => s.Id == seriesId);
return series.Seasons.Single(s => s.SeasonNumber == seasonNumber);
} }
public bool IsMonitored(int seriesId, int seasonNumber) public bool IsMonitored(int seriesId, int seasonNumber)
{ {
var season = Query.SingleOrDefault(s => s.SeriesId == seriesId && s.SeasonNumber == seasonNumber); var series = Query.Single(s => s.Id == seriesId);
return series.Seasons.Single(s => s.SeasonNumber == seasonNumber).Monitored;
if (season == null) return true;
return season.Monitored;
} }
public List<Season> GetSeasonBySeries(int seriesId) 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.

View File

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

View File

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

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -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();
}
}
}

View File

@ -2,8 +2,7 @@
<div class="row"> <div class="row">
<div class="span2"> <div class="span2">
<a href="{{traktUrl}}" target="_blank"> <a href="{{traktUrl}}" target="_blank">
<img class="new-series-poster" src="{{remotePoster}}" <img class="new-series-poster" src="{{remotePoster}}" {{defaultImg}} >
{{defaultImg}} >
</a> </a>
</div> </div>
<div class="span9"> <div class="span9">

View File

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

View File

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

View File

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

View File

@ -2,11 +2,9 @@
define( define(
[ [
'marionette', 'marionette',
'backgrid',
'Series/SeasonCollection', 'Series/SeasonCollection',
'Cells/ToggleCell',
'Shared/Actioneer' 'Shared/Actioneer'
], function (Marionette, Backgrid, SeasonCollection, ToggleCell, Actioneer) { ], function (Marionette, SeasonCollection, Actioneer) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'SeasonPass/SeriesLayoutTemplate', template: 'SeasonPass/SeriesLayoutTemplate',
@ -19,48 +17,22 @@ define(
events: { events: {
'change .x-season-select': '_seasonSelected', 'change .x-season-select': '_seasonSelected',
'click .x-expander' : '_expand', 'click .x-expander' : '_expand',
'click .x-latest' : '_latest' 'click .x-latest' : '_latest',
'click .x-monitored' : '_toggleSeasonMonitored'
}, },
regions: { regions: {
seasonGrid: '.x-season-grid' seasonGrid: '.x-season-grid'
}, },
columns: initialize: function () {
[ this.seasonCollection = new SeasonCollection(this.model.get('seasons'));
{
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);
this.expanded = false; this.expanded = false;
}, },
onRender: function () { onRender: function () {
this.seasonGrid.show(new Backgrid.Grid({
columns : this.columns,
collection: this.seasonCollection,
className : 'table table-condensed season-grid span5'
}));
if (!this.expanded) { if (!this.expanded) {
this.seasonGrid.$el.hide(); this.ui.seasonGrid.hide();
} }
this._setExpanderIcon(); this._setExpanderIcon();
@ -103,33 +75,51 @@ define(
}, },
_latest: function () { _latest: function () {
var season = _.max(this.seasonCollection.models, function (model) { var season = _.max(this.model.get('seasons'), function (s) {
return model.get('seasonNumber'); return s.seasonNumber;
}); });
//var seasonNumber = season.get('seasonNumber'); this._setMonitored(season.seasonNumber);
this._setMonitored(season.get('seasonNumber'))
}, },
_setMonitored: function (seasonNumber) { _setMonitored: function (seasonNumber) {
//TODO: use Actioneer?
var self = this; var self = this;
var promise = $.ajax({ this.model.setSeasonPass(seasonNumber);
url: this.seasonCollection.url + '/pass',
type: 'POST', var promise = this.model.save();
data: {
seriesId: this.model.get('id'),
seasonNumber: seasonNumber
}
});
promise.done(function (data) { promise.done(function (data) {
self.seasonCollection = new SeasonCollection(data); self.seasonCollection = new SeasonCollection(data);
self.render(); 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();
} }
}); });
}); });

View File

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

View File

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

View File

@ -113,12 +113,12 @@ define(
this.ui.monitored.removeClass('icon-spin icon-spinner'); this.ui.monitored.removeClass('icon-spin icon-spinner');
if (this.model.get('monitored')) { if (this.model.get('monitored')) {
this.ui.monitored.addClass('icon-bookmark'); this.ui.monitored.addClass('icon-nd-monitored');
this.ui.monitored.removeClass('icon-bookmark-empty'); this.ui.monitored.removeClass('icon-nd-unmonitored');
} }
else { else {
this.ui.monitored.addClass('icon-bookmark-empty'); this.ui.monitored.addClass('icon-nd-unmonitored');
this.ui.monitored.removeClass('icon-bookmark'); this.ui.monitored.removeClass('icon-nd-monitored');
} }
}, },
@ -176,11 +176,11 @@ define(
this.seasons.show(new LoadingView()); 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.episodeCollection = new EpisodeCollection({ seriesId: this.model.id });
this.episodeFileCollection = new EpisodeFileCollection({ 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({ var seasonCollectionView = new SeasonCollectionView({
collection : self.seasonCollection, collection : self.seasonCollection,
episodeCollection: self.episodeCollection, episodeCollection: self.episodeCollection,

View File

@ -5,21 +5,10 @@ define(
'Series/SeasonModel' 'Series/SeasonModel'
], function (Backbone, SeasonModel) { ], function (Backbone, SeasonModel) {
return Backbone.Collection.extend({ return Backbone.Collection.extend({
url : window.ApiRoot + '/season',
model: SeasonModel, model: SeasonModel,
comparator: function (season) { comparator: function (season) {
return -season.get('seasonNumber'); 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);
} }
}); });
}); });

View File

@ -14,6 +14,25 @@ define(
episodeCount : 0, episodeCount : 0,
isExisting : false, isExisting : false,
status : 0 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;
}
});
} }
}); });
}); });

View File

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

View File

@ -33,7 +33,9 @@ define(
this._showStartMessage(options); this._showStartMessage(options);
this._setSpinnerOnElement(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); this._handlePromise(promise, options);
}, },