diff --git a/Marr.Data/QGen/InsertQueryBuilder.cs b/Marr.Data/QGen/InsertQueryBuilder.cs index 6b5d40e5a..9b14d7a0b 100644 --- a/Marr.Data/QGen/InsertQueryBuilder.cs +++ b/Marr.Data/QGen/InsertQueryBuilder.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using Marr.Data.Mapping; using System.Linq.Expressions; diff --git a/Marr.Data/Reflection/SimpleReflectionStrategy.cs b/Marr.Data/Reflection/SimpleReflectionStrategy.cs index 968406bf6..8e6a964ca 100644 --- a/Marr.Data/Reflection/SimpleReflectionStrategy.cs +++ b/Marr.Data/Reflection/SimpleReflectionStrategy.cs @@ -1,20 +1,34 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Reflection; -using Marr.Data; namespace Marr.Data.Reflection { public class SimpleReflectionStrategy : IReflectionStrategy { + + private static readonly Dictionary MemberCache = new Dictionary(); + + + private static MemberInfo GetMember(Type entityType, string name) + { + MemberInfo member; + var key = entityType.FullName + name; + if (!MemberCache.TryGetValue(key, out member)) + { + member = entityType.GetMember(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)[0]; + MemberCache[key] = member; + } + + return member; + } + /// /// Sets an entity field value by name to the passed in 'val'. /// public void SetFieldValue(T entity, string fieldName, object val) { - MemberInfo member = entity.GetType().GetMember(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)[0]; + var member = GetMember(entity.GetType(), fieldName); try { @@ -22,13 +36,17 @@ namespace Marr.Data.Reflection if (val == DBNull.Value) { if (member.MemberType == MemberTypes.Field) - (member as FieldInfo).SetValue(entity, ReflectionHelper.GetDefaultValue((member as FieldInfo).FieldType)); - + { + (member as FieldInfo).SetValue(entity, + ReflectionHelper.GetDefaultValue((member as FieldInfo).FieldType)); + } else if (member.MemberType == MemberTypes.Property) { var pi = (member as PropertyInfo); if (pi.CanWrite) - (member as PropertyInfo).SetValue(entity, ReflectionHelper.GetDefaultValue((member as PropertyInfo).PropertyType), null); + (member as PropertyInfo).SetValue(entity, + ReflectionHelper.GetDefaultValue( + (member as PropertyInfo).PropertyType), null); } } @@ -58,13 +76,13 @@ namespace Marr.Data.Reflection /// public object GetFieldValue(object entity, string fieldName) { - MemberInfo member = entity.GetType().GetMember(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)[0]; + var member = GetMember(entity.GetType(), fieldName); if (member.MemberType == MemberTypes.Field) { return (member as FieldInfo).GetValue(entity); } - else if (member.MemberType == MemberTypes.Property) + if (member.MemberType == MemberTypes.Property) { if ((member as PropertyInfo).CanRead) return (member as PropertyInfo).GetValue(entity, null); diff --git a/NzbDrone.Core.Test/MetadataSourceTests/TracktProxyFixture.cs b/NzbDrone.Core.Test/MetadataSourceTests/TracktProxyFixture.cs index 62987173a..634d5faa9 100644 --- a/NzbDrone.Core.Test/MetadataSourceTests/TracktProxyFixture.cs +++ b/NzbDrone.Core.Test/MetadataSourceTests/TracktProxyFixture.cs @@ -38,25 +38,22 @@ namespace NzbDrone.Core.Test.MetadataSourceTests { var details = Subject.GetSeriesInfo(75978); - ValidateSeries(details); - } - [Test] - public void should_be_able_to_get_list_of_episodes() - { - var details = Subject.GetEpisodeInfo(75978); + ValidateSeries(details.Item1); - details.Should().NotBeEmpty(); + var episodes = details.Item2; - details.GroupBy(e => e.SeasonNumber.ToString("000") + e.EpisodeNumber.ToString("000")) + episodes.Should().NotBeEmpty(); + + episodes.GroupBy(e => e.SeasonNumber.ToString("000") + e.EpisodeNumber.ToString("000")) .Max(e => e.Count()).Should().Be(1); - details.Select(c => c.TvDbEpisodeId).Should().OnlyHaveUniqueItems(); + episodes.Select(c => c.TvDbEpisodeId).Should().OnlyHaveUniqueItems(); - details.Should().Contain(c => c.SeasonNumber > 0); - details.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Overview)); + episodes.Should().Contain(c => c.SeasonNumber > 0); + episodes.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Overview)); - foreach (var episode in details) + foreach (var episode in episodes) { episode.AirDate.Should().HaveValue(); episode.AirDate.Value.Kind.Should().Be(DateTimeKind.Utc); @@ -64,10 +61,12 @@ namespace NzbDrone.Core.Test.MetadataSourceTests episode.Title.Should().NotBeBlank(); episode.TvDbEpisodeId.Should().NotBe(0); } + } + private void ValidateSeries(Series series) { series.Should().NotBeNull(); diff --git a/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index f3d9fe7a7..d3ba7c0d6 100644 --- a/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Data; using System.Linq; using NzbDrone.Common.Messaging; using NzbDrone.Core.Datastore; @@ -11,6 +10,7 @@ namespace NzbDrone.Core.MediaFiles EpisodeFile GetFileByPath(string path); List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + bool Exists(string path); } @@ -27,6 +27,11 @@ namespace NzbDrone.Core.MediaFiles return Query.SingleOrDefault(c => c.Path == path); } + public bool Exists(string path) + { + return Query.Any(c => c.Path == path); + } + public List GetFilesBySeries(int seriesId) { return Query.Where(c => c.SeriesId == seriesId).ToList(); diff --git a/NzbDrone.Core/MediaFiles/MediaFileService.cs b/NzbDrone.Core/MediaFiles/MediaFileService.cs index ccb856329..931ada125 100644 --- a/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -23,16 +23,14 @@ namespace NzbDrone.Core.MediaFiles public class MediaFileService : IMediaFileService, IHandleAsync { private readonly IConfigService _configService; - private readonly IEpisodeService _episodeService; private readonly IMessageAggregator _messageAggregator; private readonly Logger _logger; private readonly IMediaFileRepository _mediaFileRepository; - public MediaFileService(IMediaFileRepository mediaFileRepository, IConfigService configService, IEpisodeService episodeService, IMessageAggregator messageAggregator, Logger logger) + public MediaFileService(IMediaFileRepository mediaFileRepository, IConfigService configService, IMessageAggregator messageAggregator, Logger logger) { _mediaFileRepository = mediaFileRepository; _configService = configService; - _episodeService = episodeService; _messageAggregator = messageAggregator; _logger = logger; } @@ -57,7 +55,7 @@ namespace NzbDrone.Core.MediaFiles public bool Exists(string path) { - return GetFileByPath(path) != null; + return _mediaFileRepository.Exists(path); } public EpisodeFile GetFileByPath(string path) diff --git a/NzbDrone.Core/MetadataSource/IProvideEpisodeInfo.cs b/NzbDrone.Core/MetadataSource/IProvideEpisodeInfo.cs deleted file mode 100644 index d3c8728cc..000000000 --- a/NzbDrone.Core/MetadataSource/IProvideEpisodeInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MetadataSource -{ - public interface IProvideEpisodeInfo - { - IList GetEpisodeInfo(int tvDbSeriesId); - } -} \ No newline at end of file diff --git a/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs b/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs index 2205a483e..7eb8ace28 100644 --- a/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs +++ b/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs @@ -1,9 +1,11 @@ -using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.MetadataSource { public interface IProvideSeriesInfo { - Series GetSeriesInfo(int tvDbSeriesId); + Tuple> GetSeriesInfo(int tvDbSeriesId); } } \ No newline at end of file diff --git a/NzbDrone.Core/MetadataSource/TraktProxy.cs b/NzbDrone.Core/MetadataSource/TraktProxy.cs index e2b325677..9f56bcd84 100644 --- a/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -7,12 +7,11 @@ using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.Trakt; using NzbDrone.Core.Tv; using RestSharp; -using NzbDrone.Common.EnsureThat; using Episode = NzbDrone.Core.Tv.Episode; namespace NzbDrone.Core.MetadataSource { - public class TraktProxy : ISearchForNewSeries, IProvideSeriesInfo, IProvideEpisodeInfo + public class TraktProxy : ISearchForNewSeries, IProvideSeriesInfo { public List SearchForNewSeries(string title) { @@ -23,22 +22,16 @@ namespace NzbDrone.Core.MetadataSource return response.Data.Select(MapSeries).ToList(); } - public Series GetSeriesInfo(int tvDbSeriesId) - { - var client = BuildClient("show", "summary"); - var restRequest = new RestRequest(tvDbSeriesId.ToString()); - var response = client.Execute(restRequest); - - return MapSeries(response.Data); - } - - public IList GetEpisodeInfo(int tvDbSeriesId) + public Tuple> GetSeriesInfo(int tvDbSeriesId) { var client = BuildClient("show", "summary"); var restRequest = new RestRequest(tvDbSeriesId.ToString() + "/extended"); var response = client.Execute(restRequest); - return response.Data.seasons.SelectMany(c => c.episodes).Select(MapEpisode).ToList(); + var episodes = response.Data.seasons.SelectMany(c => c.episodes).Select(MapEpisode).ToList(); + var series = MapSeries(response.Data); + + return new Tuple>(series, episodes); } private static IRestClient BuildClient(string resource, string method) diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 33039d2ce..20ef8261e 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -311,7 +311,6 @@ - @@ -513,6 +512,7 @@ + diff --git a/NzbDrone.Core/Tv/EpisodeService.cs b/NzbDrone.Core/Tv/EpisodeService.cs index 1cd41c89c..e371af1df 100644 --- a/NzbDrone.Core/Tv/EpisodeService.cs +++ b/NzbDrone.Core/Tv/EpisodeService.cs @@ -6,7 +6,6 @@ using NzbDrone.Common.Messaging; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Tv @@ -23,42 +22,32 @@ namespace NzbDrone.Core.Tv PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec); List GetEpisodesByFileId(int episodeFileId); List EpisodesWithFiles(); - void RefreshEpisodeInfo(Series series); void UpdateEpisode(Episode episode); List GetEpisodeNumbersBySeason(int seriesId, int seasonNumber); void SetEpisodeIgnore(int episodeId, bool isIgnored); bool IsFirstOrLastEpisodeOfSeason(int episodeId); void UpdateEpisodes(List episodes); List EpisodesBetweenDates(DateTime start, DateTime end); + void InsertMany(List episodes); + void UpdateMany(List episodes); } public class EpisodeService : IEpisodeService, IHandle, IHandle, - IHandleAsync, - IHandleAsync + IHandleAsync { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - private readonly IProvideEpisodeInfo _episodeInfoProxy; - private readonly ISeasonRepository _seasonRepository; private readonly IEpisodeRepository _episodeRepository; - private readonly IMessageAggregator _messageAggregator; private readonly IConfigService _configService; - private readonly ISeriesService _seriesService; private readonly Logger _logger; - public EpisodeService(IProvideEpisodeInfo episodeInfoProxy, ISeasonRepository seasonRepository, - IEpisodeRepository episodeRepository, IMessageAggregator messageAggregator, - IConfigService configService, ISeriesService seriesService, Logger logger) + public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, Logger logger) { - _episodeInfoProxy = episodeInfoProxy; - _seasonRepository = seasonRepository; _episodeRepository = episodeRepository; - _messageAggregator = messageAggregator; _configService = configService; - _seriesService = seriesService; _logger = logger; } @@ -122,121 +111,7 @@ namespace NzbDrone.Core.Tv return _episodeRepository.EpisodesWithFiles(); } - public void RefreshEpisodeInfo(Series series) - { - logger.Trace("Starting episode info refresh for series: {0}", series.Title.WithDefault(series.Id)); - var successCount = 0; - var failCount = 0; - - var tvdbEpisodes = _episodeInfoProxy.GetEpisodeInfo(series.TvdbId); - - var seriesEpisodes = GetEpisodeBySeries(series.Id); - var updateList = new List(); - var newList = new List(); - - foreach (var episode in tvdbEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) - { - try - { - logger.Trace("Updating info for [{0}] - S{1:00}E{2:00}", series.Title, episode.SeasonNumber, episode.EpisodeNumber); - - //first check using tvdbId, this should cover cases when and episode number in a season is changed - - var episodes = seriesEpisodes.Where(e => e.TvDbEpisodeId == episode.TvDbEpisodeId).ToList(); - var episodeToUpdate = seriesEpisodes.SingleOrDefault(e => e.TvDbEpisodeId == episode.TvDbEpisodeId); - - //not found, try using season/episode number - if (episodeToUpdate == null) - { - episodeToUpdate = seriesEpisodes.SingleOrDefault(e => e.SeasonNumber == episode.SeasonNumber && e.EpisodeNumber == episode.EpisodeNumber); - } - - if (episodeToUpdate == null) - { - episodeToUpdate = new Episode(); - newList.Add(episodeToUpdate); - - //If it is Episode Zero Ignore it (specials, sneak peeks.) - if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) - { - episodeToUpdate.Ignored = true; - } - else - { - episodeToUpdate.Ignored = _seasonRepository.IsIgnored(series.Id, episode.SeasonNumber); - } - } - else - { - updateList.Add(episodeToUpdate); - } - - if ((episodeToUpdate.EpisodeNumber != episode.EpisodeNumber || - episodeToUpdate.SeasonNumber != episode.SeasonNumber) && - episodeToUpdate.EpisodeFileId > 0) - { - logger.Info("Unlinking episode file because TheTVDB changed the episode number..."); - episodeToUpdate.EpisodeFileId = 0; - } - - episodeToUpdate.SeriesId = series.Id; - episodeToUpdate.TvDbEpisodeId = episode.TvDbEpisodeId; - episodeToUpdate.EpisodeNumber = episode.EpisodeNumber; - episodeToUpdate.SeasonNumber = episode.SeasonNumber; - episodeToUpdate.Title = episode.Title; - episodeToUpdate.Overview = episode.Overview; - episodeToUpdate.AirDate = episode.AirDate; - - successCount++; - } - catch (Exception e) - { - logger.FatalException(String.Format("An error has occurred while updating episode info for series {0}", series.Title), e); - failCount++; - } - } - - var allEpisodes = new List(); - allEpisodes.AddRange(newList); - allEpisodes.AddRange(updateList); - - var groups = allEpisodes.GroupBy(e => new { e.SeriesId, e.AirDate }).Where(g => g.Count() > 1).ToList(); - foreach (var group in groups) - { - int episodeCount = 0; - foreach (var episode in group.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) - { - episode.AirDate = episode.AirDate.Value.AddMinutes(series.Runtime * episodeCount); - episodeCount++; - } - } - - _episodeRepository.InsertMany(newList); - _episodeRepository.UpdateMany(updateList); - - if (newList.Any()) - { - _messageAggregator.PublishEvent(new EpisodeInfoAddedEvent(newList, series)); - } - - if (updateList.Any()) - { - _messageAggregator.PublishEvent(new EpisodeInfoUpdatedEvent(updateList)); - } - - if (failCount != 0) - { - logger.Info("Finished episode refresh for series: {0}. Successful: {1} - Failed: {2} ", - series.Title, successCount, failCount); - } - else - { - logger.Info("Finished episode refresh for series: {0}.", series.Title); - } - - DeleteEpisodesNotInTvdb(series, tvdbEpisodes); - } public void UpdateEpisode(Episode episode) { @@ -282,6 +157,16 @@ namespace NzbDrone.Core.Tv return episodes; } + public void InsertMany(List episodes) + { + _episodeRepository.InsertMany(episodes); + } + + public void UpdateMany(List episodes) + { + _episodeRepository.InsertMany(episodes); + } + public void HandleAsync(SeriesDeletedEvent message) { var episodes = GetEpisodeBySeries(message.Series.Id); @@ -299,11 +184,6 @@ namespace NzbDrone.Core.Tv } } - public void HandleAsync(SeriesAddedEvent message) - { - RefreshEpisodeInfo(message.Series); - } - public void Handle(EpisodeFileAddedEvent message) { foreach (var episode in message.EpisodeFile.Episodes.Value) @@ -313,17 +193,6 @@ namespace NzbDrone.Core.Tv } } - private void DeleteEpisodesNotInTvdb(Series series, IEnumerable tvdbEpisodes) - { - //Todo: This will not work as currently implemented - what are we trying to do here? - return; - logger.Trace("Starting deletion of episodes that no longer exist in TVDB: {0}", series.Title.WithDefault(series.Id)); - foreach (var episode in tvdbEpisodes) - { - _episodeRepository.Delete(episode.Id); - } - logger.Trace("Deleted episodes that no longer exist in TVDB for {0}", series.Id); - } } } \ No newline at end of file diff --git a/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs b/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs index 08cbe5c23..8dafe0563 100644 --- a/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs +++ b/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs @@ -1,5 +1,4 @@ -using System.Linq; -using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Tv.Events { diff --git a/NzbDrone.Core/Tv/RefreshSeriesService.cs b/NzbDrone.Core/Tv/RefreshSeriesService.cs new file mode 100644 index 000000000..b7a1885c6 --- /dev/null +++ b/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class RefreshSeriesService : IExecute, IHandleAsync + { + private readonly IProvideSeriesInfo _seriesInfo; + private readonly ISeriesService _seriesService; + private readonly IEpisodeService _episodeService; + private readonly ISeasonRepository _seasonRepository; + private readonly IMessageAggregator _messageAggregator; + private readonly Logger _logger; + + public RefreshSeriesService(IProvideSeriesInfo seriesInfo, ISeriesService seriesService, IEpisodeService episodeService, + ISeasonRepository seasonRepository, IMessageAggregator messageAggregator, Logger logger) + { + _seriesInfo = seriesInfo; + _seriesService = seriesService; + _episodeService = episodeService; + _seasonRepository = seasonRepository; + _messageAggregator = messageAggregator; + _logger = logger; + } + + + public void Execute(RefreshSeriesCommand message) + { + if (message.SeriesId.HasValue) + { + RefreshSeriesInfo(message.SeriesId.Value); + } + else + { + var ids = _seriesService.GetAllSeries().OrderBy(c => c.LastInfoSync).Select(c => c.Id).ToList(); + + foreach (var id in ids) + { + RefreshSeriesInfo(id); + } + } + + } + + public void HandleAsync(SeriesAddedEvent message) + { + RefreshSeriesInfo(message.Series.Id); + } + + private Series RefreshSeriesInfo(int seriesId) + { + var series = _seriesService.GetSeries(seriesId); + var tuple = _seriesInfo.GetSeriesInfo(series.TvdbId); + + var seriesInfo = tuple.Item1; + + series.Title = seriesInfo.Title; + series.AirTime = seriesInfo.AirTime; + series.Overview = seriesInfo.Overview; + series.Status = seriesInfo.Status; + series.CleanTitle = Parser.Parser.NormalizeTitle(seriesInfo.Title); + series.LastInfoSync = DateTime.Now; + series.Runtime = seriesInfo.Runtime; + series.Images = seriesInfo.Images; + series.Network = seriesInfo.Network; + series.FirstAired = seriesInfo.FirstAired; + _seriesService.UpdateSeries(series); + + //Todo: We need to get the UtcOffset from TVRage, since its not available from trakt + + RefreshEpisodeInfo(series, tuple.Item2); + + _messageAggregator.PublishEvent(new SeriesUpdatedEvent(series)); + return series; + } + + private void RefreshEpisodeInfo(Series series, List remoteEpisodes) + { + _logger.Trace("Starting episode info refresh for series: {0}", series.Title.WithDefault(series.Id)); + var successCount = 0; + var failCount = 0; + + + var seriesEpisodes = _episodeService.GetEpisodeBySeries(series.Id); + + var seasons = _seasonRepository.GetSeasonBySeries(series.Id); + + var updateList = new List(); + var newList = new List(); + + foreach (var episode in remoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) + { + try + { + _logger.Trace("Updating info for [{0}] - S{1:00}E{2:00}", series.Title, episode.SeasonNumber, episode.EpisodeNumber); + + var episodeToUpdate = seriesEpisodes.SingleOrDefault(e => e.TvDbEpisodeId == episode.TvDbEpisodeId); + + //not found, try using season/episode number + if (episodeToUpdate == null) + { + episodeToUpdate = seriesEpisodes.SingleOrDefault(e => e.SeasonNumber == episode.SeasonNumber && e.EpisodeNumber == episode.EpisodeNumber); + } + + if (episodeToUpdate == null) + { + episodeToUpdate = new Episode(); + newList.Add(episodeToUpdate); + + //If it is Episode Zero Ignore it (specials, sneak peeks.) + if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) + { + episodeToUpdate.Ignored = true; + } + else + { + var season = seasons.FirstOrDefault(c => c.SeasonNumber == episode.SeasonNumber); + episodeToUpdate.Ignored = season != null && season.Ignored; + } + } + else + { + updateList.Add(episodeToUpdate); + } + + if ((episodeToUpdate.EpisodeNumber != episode.EpisodeNumber || + episodeToUpdate.SeasonNumber != episode.SeasonNumber) && + episodeToUpdate.EpisodeFileId > 0) + { + _logger.Info("Unlinking episode file because the episode number has changed"); + episodeToUpdate.EpisodeFileId = 0; + } + + episodeToUpdate.SeriesId = series.Id; + episodeToUpdate.TvDbEpisodeId = episode.TvDbEpisodeId; + episodeToUpdate.EpisodeNumber = episode.EpisodeNumber; + episodeToUpdate.SeasonNumber = episode.SeasonNumber; + episodeToUpdate.Title = episode.Title; + episodeToUpdate.Overview = episode.Overview; + episodeToUpdate.AirDate = episode.AirDate; + + successCount++; + } + catch (Exception e) + { + _logger.FatalException(String.Format("An error has occurred while updating episode info for series {0}", series.Title), e); + failCount++; + } + } + + var allEpisodes = new List(); + allEpisodes.AddRange(newList); + allEpisodes.AddRange(updateList); + + var groups = allEpisodes.GroupBy(e => new { e.SeriesId, e.AirDate }).Where(g => g.Count() > 1).ToList(); + + foreach (var group in groups) + { + int episodeCount = 0; + foreach (var episode in group.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) + { + episode.AirDate = episode.AirDate.Value.AddMinutes(series.Runtime * episodeCount); + episodeCount++; + } + } + + _episodeService.InsertMany(newList); + _episodeService.UpdateMany(updateList); + + if (newList.Any()) + { + _messageAggregator.PublishEvent(new EpisodeInfoAddedEvent(newList, series)); + } + + if (updateList.Any()) + { + _messageAggregator.PublishEvent(new EpisodeInfoUpdatedEvent(updateList)); + } + + if (failCount != 0) + { + _logger.Info("Finished episode refresh for series: {0}. Successful: {1} - Failed: {2} ", + series.Title, successCount, failCount); + } + else + { + _logger.Info("Finished episode refresh for series: {0}.", series.Title); + } + + //DeleteEpisodesNotAvailableAnymore(series, remoteEpisodes); + } + + + /* private void DeleteEpisodesNotAvailableAnymore(Series series, IEnumerable onlineEpisodes) + { + //Todo: This will not work as currently implemented - what are we trying to do here? + return; + _logger.Trace("Starting deletion of episodes that no longer exist in TVDB: {0}", series.Title.WithDefault(series.Id)); + foreach (var episode in onlineEpisodes) + { + _episodeRepository.Delete(episode.Id); + } + + _logger.Trace("Deleted episodes that no longer exist in TVDB for {0}", series.Id); + }*/ + } + + public class RefreshSeriesCommand : ICommand + { + public int? SeriesId { get; private set; } + + public RefreshSeriesCommand(int? seriesId) + { + SeriesId = seriesId; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Tv/SeriesService.cs b/NzbDrone.Core/Tv/SeriesService.cs index dd991718d..131900626 100644 --- a/NzbDrone.Core/Tv/SeriesService.cs +++ b/NzbDrone.Core/Tv/SeriesService.cs @@ -8,10 +8,8 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Messaging; using NzbDrone.Core.Configuration; using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Model; using NzbDrone.Core.Organizer; -using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Tv @@ -19,7 +17,6 @@ namespace NzbDrone.Core.Tv public interface ISeriesService { bool IsMonitored(int id); - Series UpdateSeriesInfo(int seriesId); Series GetSeries(int seriesId); Series AddSeries(Series newSeries); void UpdateFromSeriesEditor(IList editedSeries); @@ -34,27 +31,21 @@ namespace NzbDrone.Core.Tv Series FindBySlug(string slug); } - public class SeriesService : ISeriesService, IHandleAsync + public class SeriesService : ISeriesService { private readonly ISeriesRepository _seriesRepository; private readonly IConfigService _configService; - private readonly IProvideSeriesInfo _seriesInfoProxy; private readonly IMessageAggregator _messageAggregator; private readonly ISceneMappingService _sceneMappingService; - private readonly IRootFolderService _rootFolderService; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - public SeriesService(ISeriesRepository seriesRepository, IConfigService configServiceService, - IProvideSeriesInfo seriesInfoProxy, IMessageAggregator messageAggregator, ISceneMappingService sceneMappingService, - IRootFolderService rootFolderService, IDiskProvider diskProvider, Logger logger) + public SeriesService(ISeriesRepository seriesRepository, IConfigService configServiceService, IMessageAggregator messageAggregator, ISceneMappingService sceneMappingService, IDiskProvider diskProvider, Logger logger) { _seriesRepository = seriesRepository; _configService = configServiceService; - _seriesInfoProxy = seriesInfoProxy; _messageAggregator = messageAggregator; _sceneMappingService = sceneMappingService; - _rootFolderService = rootFolderService; _diskProvider = diskProvider; _logger = logger; } @@ -64,29 +55,7 @@ namespace NzbDrone.Core.Tv return _seriesRepository.Get(id).Monitored; } - public Series UpdateSeriesInfo(int seriesId) - { - var series = _seriesRepository.Get(seriesId); - var seriesInfo = _seriesInfoProxy.GetSeriesInfo(series.TvdbId); - - series.Title = seriesInfo.Title; - series.AirTime = seriesInfo.AirTime; - series.Overview = seriesInfo.Overview; - series.Status = seriesInfo.Status; - series.CleanTitle = Parser.Parser.NormalizeTitle(seriesInfo.Title); - series.LastInfoSync = DateTime.Now; - series.Runtime = seriesInfo.Runtime; - series.Images = seriesInfo.Images; - series.Network = seriesInfo.Network; - series.FirstAired = seriesInfo.FirstAired; - _seriesRepository.Update(series); - - //Todo: We need to get the UtcOffset from TVRage, since its not available from trakt - - _messageAggregator.PublishEvent(new SeriesUpdatedEvent(series)); - return series; - } public Series GetSeries(int seriesId) { @@ -194,10 +163,5 @@ namespace NzbDrone.Core.Tv { return _seriesRepository.Get(seriesIds).ToList(); } - - public void HandleAsync(SeriesAddedEvent message) - { - UpdateSeriesInfo(message.Series.Id); - } } } diff --git a/UI/Content/theme.less b/UI/Content/theme.less index 92b167378..09af6eafb 100644 --- a/UI/Content/theme.less +++ b/UI/Content/theme.less @@ -1,14 +1,17 @@ .settings-group { margin-top: 40px; } + .tag:hover { background-color: #008dcd; color: #ffffff; text-decoration: none; } + .panel .danger, .panel .danger h6 { background-color: #ea494a; } + #in-nav { background-color: white; background-repeat: repeat; @@ -26,15 +29,19 @@ } } } + #calendar th, #calendar td { border-color: #eeeeee; } + .panel .success, .panel .success h6 { background-color: #4cb158; } + .panel .warning, .panel .warning h6 { background-color: #ffa93c; } + .panel { width: 150px; background-color: #eeeeee; @@ -83,6 +90,7 @@ } } } + .peity { margin-bottom: 20px; h4 { @@ -99,14 +107,17 @@ color: #999999; } } + .star { background-position: -192px -96px; } + .small-divide { border-top: 1px solid #eeeeee; margin-top: 20px; padding-top: 20px; } + body { background-color: #252525; background-image: url('../content/images/pattern.png'); @@ -117,9 +128,11 @@ body { font-size: 0.9em; } } + .popover { width: 276px; } + .tag { display: inline-block; background-color: #eeeeee; @@ -128,6 +141,7 @@ body { margin-bottom: 5px; border-radius: 4px; } + #calendar { .primary { border-color: #007ccd; @@ -173,6 +187,7 @@ body { font-size: 17.5px; } } + .event { display: inline-block; width: 100%; @@ -227,6 +242,7 @@ body { border-color: #4cb158; } } + .profile-sidebar { ul { margin: 0; @@ -241,6 +257,7 @@ body { } } } + .table-panel { table { border-top: 1px solid #eeeeee; @@ -249,15 +266,19 @@ body { margin: 4px; } } + .progress-small { height: 8px; } + .calendar { background-position: -160px -128px; } + .quill { background-position: -288px 0px; } + .stream { background-position: 0 -64px; .item { @@ -315,9 +336,11 @@ body { } } } + .forms { background-position: -96px -32px; } + .filters { h5 { margin-top: 20px; @@ -332,12 +355,15 @@ body { color: #999999; } } + .panel .primary, .panel .primary h6 { background-color: #007ccd; } + .panel .info, .panel .info h6 { background-color: #14b8d4; } + .spark { margin-bottom: 20px; h4 { @@ -359,10 +385,12 @@ body { } } } + .widget td.bar-number, .widget td.bar-percent { width: 15%; font-weight: 600; } + ul.stat-list { margin: 0; li { @@ -389,6 +417,7 @@ ul.stat-list { } } } + .messages { h4 { text-transform: none; @@ -411,21 +440,27 @@ ul.stat-list { } } } + .message-sidebar a.message-preview p, .message-sidebar a.message-preview h5 { color: #999999; } + .b-alarm { background-position: -384px 0px; } + .users { background-position: 0 -96px; } + .home { background-position: -128px -96px; } + .no-star { background-position: 0 -16px; } + .sidebar { border-top: 1px solid #eeeeee; ul { @@ -450,26 +485,32 @@ ul.stat-list { } } } + .login { padding: 20px; margin-top: 40px; margin-bottom: 80px; } + .b-wifi { background-position: -256px -64px; } + .plane { background-position: -256px -96px; } + .batch { background-image: url(images/white-batch-32.png); display: inline-block; height: 32px; width: 32px; } + .b-code { background-position: -64px 0px; } + .widget { background-color: #eeeeee; color: #555555; @@ -500,12 +541,15 @@ ul.stat-list { font-weight: 300; } } + ul.stat-list li label, ul.stat-list li h4, ul.stat-list li small, ul.stat-list li p { display: inline-block; } + .b-flag { background-position: -384px -256px; } + footer { padding-top: 20px; padding-bottom: 20px; @@ -514,9 +558,11 @@ footer { color: #999999; } } + .popover-title { text-transform: none; } + .qa { border-bottom: 1px solid #eeeeee; padding-bottom: 20px; @@ -527,40 +573,50 @@ footer { font-weight: 400; } } + .panel .purple, .panel .purple h6 { background-color: #7932ea; } + .knob { text-align: center; } + .message-sidebar a.active p, .message-sidebar a.active h5 { color: rgba(255, 255, 255, 0.8); } + .b-comment { background-position: -192px -256px; } + #in-sub-nav li a:hover, #in-sub-nav li a.active { background-color: #555555; text-decoration: none; } + body h1, body h2, body h3, body h4, body h5, body h6 { text-transform: capitalize; font-weight: 300; } + .b-database { background-position: -256px 0px; } + .batch-big { background-image: url(images/white-batch-64.png); display: inline-block; height: 64px; width: 64px; } + .divide { border-top: 1px solid #eeeeee; margin-top: 40px; padding-top: 40px; } + .page { width: 1210px; min-width: 1210px; @@ -576,6 +632,7 @@ body h1, body h2, body h3, body h4, body h5, body h6 { border-bottom: 1px solid #eeeeee; } } + #events { .load-more { text-align: center; @@ -583,15 +640,18 @@ body h1, body h2, body h3, body h4, body h5, body h6 { height: 600px; } } + .half-star { background-position: 0 -32px; } + .rating-star, .no-star, .half-star { display: inline-block; height: 16px; width: 16px; background-image: url(../img/stars.png); } + .stats { text-align: center; .primary { @@ -637,6 +697,7 @@ body h1, body h2, body h3, body h4, body h5, body h6 { color: #ffffff; } } + .message-sidebar { border-right: 1px solid #eeeeee; margin: -20px; @@ -672,9 +733,11 @@ body h1, body h2, body h3, body h4, body h5, body h6 { background-color: #ffffff; } } + .settings { background-position: -288px -128px; } + .tables { background-position: -32px -32px; code { @@ -682,6 +745,7 @@ body h1, body h2, body h3, body h4, body h5, body h6 { margin: 2px; } } + #in-sub-nav { ul { text-align: center; diff --git a/UI/Series/Index/SeriesIndexLayout.js b/UI/Series/Index/SeriesIndexLayout.js index 296d113b8..b23cb7e90 100644 --- a/UI/Series/Index/SeriesIndexLayout.js +++ b/UI/Series/Index/SeriesIndexLayout.js @@ -140,7 +140,7 @@ define([ { title : 'Update Library', icon : 'icon-refresh', - command : 'diskscan', + command : 'refreshseries', successMessage: 'Library was updated!', errorMessage : 'Library update failed!' },