diff --git a/src/NzbDrone.Api/Notifications/NotificationModule.cs b/src/NzbDrone.Api/Notifications/NotificationModule.cs index 62fedc40a..fb6130e2c 100644 --- a/src/NzbDrone.Api/Notifications/NotificationModule.cs +++ b/src/NzbDrone.Api/Notifications/NotificationModule.cs @@ -2,9 +2,9 @@ namespace NzbDrone.Api.Notifications { - public class IndexerModule : ProviderModuleBase + public class NotificationModule : ProviderModuleBase { - public IndexerModule(NotificationFactory notificationFactory) + public NotificationModule(NotificationFactory notificationFactory) : base(notificationFactory, "notification") { } diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs index 7ae8c326f..73d783af2 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs @@ -6,6 +6,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Common.Test.DiskProviderTests { public class IsParentFixtureBase : TestBase where TSubject : class, IDiskProvider + public class IsParentFixture : TestBase { private string _parent = @"C:\Test".AsOsAgnostic(); @@ -14,7 +15,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests { var path = @"C:\Another Folder".AsOsAgnostic(); - Subject.IsParent(_parent, path).Should().BeFalse(); + DiskProvider.IsParent(_parent, path).Should().BeFalse(); } [Test] @@ -22,7 +23,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests { var path = @"C:\Test\TV".AsOsAgnostic(); - Subject.IsParent(_parent, path).Should().BeTrue(); + DiskProvider.IsParent(_parent, path).Should().BeTrue(); } [Test] @@ -30,7 +31,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests { var path = @"C:\Test\30.Rock.S01E01.Pilot.avi".AsOsAgnostic(); - Subject.IsParent(_parent, path).Should().BeTrue(); + DiskProvider.IsParent(_parent, path).Should().BeTrue(); } } } diff --git a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs index 1286d4b70..5a473fd74 100644 --- a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs +++ b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Common.Composition Container = new Container(new TinyIoCContainer(), _loadedTypes); AutoRegisterInterfaces(); Container.Register(args); - } + } private void AutoRegisterInterfaces() { diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index b1af0ccaa..eb7d95e07 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -5,12 +5,14 @@ using System.Security.AccessControl; using System.Security.Principal; using NLog; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Exceptions; using NzbDrone.Common.Instrumentation; namespace NzbDrone.Common.Disk { public abstract class DiskProviderBase : IDiskProvider { + void CopyFile(string source, string destination, bool overwrite = false); enum TransferAction { Copy, @@ -24,6 +26,41 @@ namespace NzbDrone.Common.Disk public abstract void SetPermissions(string path, string mask, string user, string group); public abstract long? GetTotalSize(string path); + //TODO: this needs tests + public static string GetRelativePath(string parentPath, string childPath) + { + if (!IsParent(parentPath, childPath)) + { + throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); + } + + var parentUri = new Uri(parentPath, UriKind.Absolute); + var childUri = new Uri(childPath, UriKind.Absolute); + + return childUri.MakeRelativeUri(parentUri).ToString(); + } + + public static bool IsParent(string parentPath, string childPath) + { + parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); + childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); + + var parent = new DirectoryInfo(parentPath); + var child = new DirectoryInfo(childPath); + + while (child.Parent != null) + { + if (child.Parent.FullName == parent.FullName) + { + return true; + } + + child = child.Parent; + } + + return false; + } + public DateTime GetLastFolderWrite(string path) { Ensure.That(path, () => path).IsValidPath(); @@ -238,6 +275,11 @@ namespace NzbDrone.Common.Disk File.Move(source, destination); } + public void CopyFile(string source, string destination, bool overwrite = false) + { + File.Copy(source, destination, overwrite); + } + public void DeleteFolder(string path, bool recursive) { Ensure.That(path, () => path).IsValidPath(); @@ -333,27 +375,6 @@ namespace NzbDrone.Common.Disk } - public bool IsParent(string parentPath, string childPath) - { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); - - var parent = new DirectoryInfo(parentPath); - var child = new DirectoryInfo(childPath); - - while (child.Parent != null) - { - if (child.Parent.FullName == parent.FullName) - { - return true; - } - - child = child.Parent; - } - - return false; - } - public void SetFolderWriteTime(string path, DateTime time) { Directory.SetLastWriteTimeUtc(path, time); diff --git a/src/NzbDrone.Common/Exceptions/NotParentException.cs b/src/NzbDrone.Common/Exceptions/NotParentException.cs new file mode 100644 index 000000000..62d594e60 --- /dev/null +++ b/src/NzbDrone.Common/Exceptions/NotParentException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Exceptions +{ + public class NotParentException : NzbDroneException + { + public NotParentException(string message, params object[] args) : base(message, args) + { + } + + public NotParentException(string message) : base(message) + { + } + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 9378ab2f6..210c9f1c9 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -91,6 +91,7 @@ + diff --git a/src/NzbDrone.Core/Datastore/Migration/036_add_metadata_to_episodes_and_series.cs b/src/NzbDrone.Core/Datastore/Migration/036_add_metadata_to_episodes_and_series.cs new file mode 100644 index 000000000..aa1a43c59 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/036_add_metadata_to_episodes_and_series.cs @@ -0,0 +1,22 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(36)] + public class add_metadata_to_episodes_and_series : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series") + .AddColumn("Actors").AsString().Nullable() + .AddColumn("Ratings").AsString().Nullable() + .AddColumn("Genres").AsString().Nullable() + .AddColumn("Certification").AsString().Nullable(); + + Alter.Table("Episodes") + .AddColumn("Ratings").AsString().Nullable() + .AddColumn("Images").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_episode_file_metadata.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_episode_file_metadata.cs new file mode 100644 index 000000000..fb0ff975e --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_episode_file_metadata.cs @@ -0,0 +1,19 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(37)] + public class add_episode_file_metadata : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("EpisodeFileMetaData") + .WithColumn("SeriesId").AsInt32().NotNullable() + .WithColumn("EpisodeFileId").AsInt32().NotNullable() + .WithColumn("Provider").AsString().NotNullable() + .WithColumn("Type").AsInt32().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable() + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_metadata_consumers.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_metadata_consumers.cs new file mode 100644 index 000000000..b5db4eac9 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_metadata_consumers.cs @@ -0,0 +1,19 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(37)] + public class add_metadata_consumers : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("MetadataConsumers") + .WithColumn("Enable").AsBoolean().NotNullable() + .WithColumn("Name").AsString().NotNullable() + .WithColumn("Implementation").AsString().NotNullable() + .WithColumn("Settings").AsString().NotNullable() + .WithColumn("ConfigContract").AsString().NotNullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f97514f16..1aa98ced4 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Metadata; using NzbDrone.Core.Notifications; using NzbDrone.Core.Organizer; using NzbDrone.Core.Qualities; @@ -35,8 +36,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("Indexers"); Mapper.Entity().RegisterModel("ScheduledTasks"); - Mapper.Entity() - .RegisterModel("Notifications"); + Mapper.Entity().RegisterModel("Notifications"); + Mapper.Entity().RegisterModel("MetadataConsumers"); Mapper.Entity().RegisterModel("SceneMappings"); @@ -85,6 +86,7 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); } private static void RegisterProviderSettingConverter() diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 4b3282b8f..28e8c0c72 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -120,6 +120,11 @@ namespace NzbDrone.Core.History public void Handle(EpisodeImportedEvent message) { + if (message.NewDownload) + { + return; + } + foreach (var episode in message.EpisodeInfo.Episodes) { var history = new History diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index e8712230e..7e92fd89d 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -14,14 +14,12 @@ namespace NzbDrone.Core.Indexers public class IndexerFactory : ProviderFactory, IIndexerFactory { private readonly IIndexerRepository _providerRepository; - private readonly IEnumerable _providers; private readonly INewznabTestService _newznabTestService; public IndexerFactory(IIndexerRepository providerRepository, IEnumerable providers, IContainer container, INewznabTestService newznabTestService, Logger logger) : base(providerRepository, providers, container, logger) { _providerRepository = providerRepository; - _providers = providers; _newznabTestService = newznabTestService; } diff --git a/src/NzbDrone.Core/MediaCover/MediaCover.cs b/src/NzbDrone.Core/MediaCover/MediaCover.cs index 8c0df5443..4b4f54b00 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCover.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCover.cs @@ -8,12 +8,24 @@ namespace NzbDrone.Core.MediaCover Unknown = 0, Poster = 1, Banner = 2, - Fanart = 3 + Fanart = 3, + Screenshot = 4, + Headshot = 5 } public class MediaCover : IEmbeddedDocument { public MediaCoverTypes CoverType { get; set; } public string Url { get; set; } + + public MediaCover() + { + } + + public MediaCover(MediaCoverTypes coverType, string url) + { + CoverType = coverType; + Url = url; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index cb81150b4..f9b0ebbc5 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -13,6 +13,12 @@ using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.MediaCover { + public interface IMapCoversToLocal + { + void ConvertToLocalUrls(int seriesId, IEnumerable covers); + string GetCoverPath(int seriesId, MediaCoverTypes mediaCoverTypes); + } + public class MediaCoverService : IHandleAsync, IHandleAsync, @@ -38,9 +44,30 @@ namespace NzbDrone.Core.MediaCover _coverRootFolder = appFolderInfo.GetMediaCoverPath(); } - public void HandleAsync(SeriesUpdatedEvent message) + public string GetCoverPath(int seriesId, MediaCoverTypes coverTypes) { - EnsureCovers(message.Series); + return Path.Combine(GetSeriesCoverPath(seriesId), coverTypes.ToString().ToLower() + ".jpg"); + } + + public void ConvertToLocalUrls(int seriesId, IEnumerable covers) + { + foreach (var mediaCover in covers) + { + var filePath = GetCoverPath(seriesId, mediaCover.CoverType); + + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + seriesId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; + + if (_diskProvider.FileExists(filePath)) + { + var lastWrite = _diskProvider.GetLastFileWrite(filePath); + mediaCover.Url += "?lastWrite=" + lastWrite.Ticks; + } + } + } + + private string GetSeriesCoverPath(int seriesId) + { + return Path.Combine(_coverRootFolder, seriesId.ToString()); } private void EnsureCovers(Series series) @@ -75,6 +102,11 @@ namespace NzbDrone.Core.MediaCover } + public void HandleAsync(SeriesUpdatedEvent message) + { + EnsureCovers(message.Series); + } + public void HandleAsync(SeriesDeletedEvent message) { var path = GetSeriesCoverPath(message.Series.Id); @@ -83,36 +115,5 @@ namespace NzbDrone.Core.MediaCover _diskProvider.DeleteFolder(path, true); } } - - private string GetCoverPath(int seriesId, MediaCoverTypes coverTypes) - { - return Path.Combine(GetSeriesCoverPath(seriesId), coverTypes.ToString().ToLower() + ".jpg"); - } - - private string GetSeriesCoverPath(int seriesId) - { - return Path.Combine(_coverRootFolder, seriesId.ToString()); - } - - public void ConvertToLocalUrls(int seriesId, IEnumerable covers) - { - foreach (var mediaCover in covers) - { - var filePath = GetCoverPath(seriesId, mediaCover.CoverType); - - mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + seriesId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; - - if (_diskProvider.FileExists(filePath)) - { - var lastWrite = _diskProvider.GetLastFileWrite(filePath); - mediaCover.Url += "?lastWrite=" + lastWrite.Ticks; - } - } - } - } - - public interface IMapCoversToLocal - { - void ConvertToLocalUrls(int seriesId, IEnumerable covers); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index a18b435c9..713e45a7d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -87,9 +87,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _mediaFileService.Add(episodeFile); imported.Add(importDecision); + _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload)); + if (newDownload) { - _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile)); _eventAggregator.PublishEvent(new EpisodeDownloadedEvent(localEpisode, episodeFile, oldFiles)); } } diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs index 38db811a6..d7c1ee6e7 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs @@ -1,4 +1,5 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.Events @@ -7,11 +8,13 @@ namespace NzbDrone.Core.MediaFiles.Events { public LocalEpisode EpisodeInfo { get; private set; } public EpisodeFile ImportedEpisode { get; private set; } + public Boolean NewDownload { get; set; } - public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode) + public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload) { EpisodeInfo = episodeInfo; ImportedEpisode = importedEpisode; + NewDownload = newDownload; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index 4a7af0d36..1c5f1c89b 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.MediaFiles continue; } - if (!_diskProvider.IsParent(series.Path, episodeFile.Path)) + if (!DiskProvider.IsParent(series.Path, episodeFile.Path)) { _logger.Trace("File [{0}] does not belong to this series, removing from db", episodeFile.Path); _mediaFileService.Delete(episodeFile); diff --git a/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs b/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs new file mode 100644 index 000000000..23c0c1653 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Consumers.Fake +{ + public class FakeMetadata : MetadataConsumerBase + { + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly Logger _logger; + + public FakeMetadata(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) + : base(diskProvider, httpProvider, logger) + { + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _logger = logger; + } + + public override void OnSeriesUpdated(Series series) + { + throw new NotImplementedException(); + } + + public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) + { + throw new NotImplementedException(); + } + + public override void AfterRename(Series series) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Consumers/Fake/FakeSettings.cs b/src/NzbDrone.Core/MetaData/Consumers/Fake/FakeSettings.cs new file mode 100644 index 000000000..e94f4a589 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Fake/FakeSettings.cs @@ -0,0 +1,41 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Metadata.Consumers.Fake +{ + public class FakeMetadataSettingsValidator : AbstractValidator + { + public FakeMetadataSettingsValidator() + { + } + } + + public class FakeMetadataSettings : IProviderConfig + { + private static readonly FakeMetadataSettingsValidator Validator = new FakeMetadataSettingsValidator(); + + public FakeMetadataSettings() + { + FakeSetting = true; + } + + [FieldDefinition(0, Label = "Fake Setting", Type = FieldType.Checkbox)] + public Boolean FakeSetting { get; set; } + + public bool IsValid + { + get + { + return true; + } + } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs new file mode 100644 index 000000000..21ee28326 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -0,0 +1,232 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Metadata.Events; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Consumers.Xbmc +{ + public class XbmcMetadata : MetadataConsumerBase + { + private readonly IEventAggregator _eventAggregator; + private readonly IMapCoversToLocal _mediaCoverService; + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly Logger _logger; + + public XbmcMetadata(IEventAggregator eventAggregator, + IMapCoversToLocal mediaCoverService, + IDiskProvider diskProvider, + IHttpProvider httpProvider, + Logger logger) + : base(diskProvider, httpProvider, logger) + { + _eventAggregator = eventAggregator; + _mediaCoverService = mediaCoverService; + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _logger = logger; + } + + public override void OnSeriesUpdated(Series series) + { + if (Settings.SeriesMetadata) + { + EnsureFolder(series.Path); + WriteTvShowNfo(series); + } + + if (Settings.SeriesImages) + { + EnsureFolder(series.Path); + WriteSeriesImages(series); + } + + if (Settings.SeasonImages) + { + EnsureFolder(series.Path); + WriteSeasonImages(series); + } + } + + public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) + { + if (Settings.EpisodeMetadata) + { + WriteEpisodeNfo(episodeFile); + } + + if (Settings.EpisodeImages) + { + WriteEpisodeImages(series, episodeFile); + } + } + + public override void AfterRename(Series series) + { + //TODO: Rename media files to match episode files + + throw new NotImplementedException(); + } + + private void WriteTvShowNfo(Series series) + { + _logger.Trace("Generating tvshow.nfo for: {0}", series.Title); + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var tvShow = new XElement("tvshow"); + + tvShow.Add(new XElement("title", series.Title)); + tvShow.Add(new XElement("rating", series.Ratings.Percentage)); + tvShow.Add(new XElement("plot", series.Overview)); + + //Todo: probably will need to use TVDB to use this feature... +// tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); +// tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); + tvShow.Add(new XElement("mpaa", series.Certification)); + tvShow.Add(new XElement("id", series.TvdbId)); + + foreach (var genre in series.Genres) + { + tvShow.Add(new XElement("genre", genre)); + } + + if (series.FirstAired.HasValue) + { + tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd"))); + } + + tvShow.Add(new XElement("studio", series.Network)); + + foreach (var actor in series.Actors) + { + tvShow.Add(new XElement("actor", + new XElement("name", actor.Name), + new XElement("role", actor.Character), + new XElement("thumb", actor.Images.First()) + )); + } + + var doc = new XDocument(tvShow); + doc.Save(xw); + + _logger.Debug("Saving tvshow.nfo for {0}", series.Title); + + var path = Path.Combine(series.Path, "tvshow.nfo"); + + _diskProvider.WriteAllText(path, doc.ToString()); + + _eventAggregator.PublishEvent(new SeriesMetadataUpdated(series, GetType().Name, MetadataType.SeriesMetadata, DiskProvider.GetRelativePath(series.Path, path))); + } + } + + private void WriteSeriesImages(Series series) + { + foreach (var image in series.Images) + { + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = Path.Combine(series.Path, image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source)); + + //TODO: Do we want to overwrite the file if it exists? + if (_diskProvider.FileExists(destination)) + { + _logger.Trace("Series image: {0} already exists.", image.CoverType); + continue; + } + + _diskProvider.CopyFile(source, destination, false); + _eventAggregator.PublishEvent(new SeriesMetadataUpdated(series, GetType().Name, MetadataType.SeriesImage, DiskProvider.GetRelativePath(series.Path, destination))); + } + } + + private void WriteSeasonImages(Series series) + { + foreach (var season in series.Seasons) + { + foreach (var image in season.Images) + { + var filename = String.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower()); + + if (season.SeasonNumber == 0) + { + filename = String.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower()); + } + + var path = Path.Combine(series.Path, filename); + + DownloadImage(series, image.Url, path); + _eventAggregator.PublishEvent(new SeasonMetadataUpdated(series, season.SeasonNumber, GetType().Name, MetadataType.SeasonImage, DiskProvider.GetRelativePath(series.Path, path))); + } + } + } + + private void WriteEpisodeNfo(EpisodeFile episodeFile) + { + var filename = episodeFile.Path.Replace(Path.GetExtension(episodeFile.Path), ".nfo"); + + _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); + + var xmlResult = String.Empty; + foreach (var episode in episodeFile.Episodes.Value) + { + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var doc = new XDocument(); + + var details = new XElement("episodedetails"); + details.Add(new XElement("title", episode.Title)); + details.Add(new XElement("season", episode.SeasonNumber)); + details.Add(new XElement("episode", episode.EpisodeNumber)); + details.Add(new XElement("aired", episode.AirDate)); + details.Add(new XElement("plot", episode.Overview)); + details.Add(new XElement("displayseason", episode.SeasonNumber)); + details.Add(new XElement("displayepisode", episode.EpisodeNumber)); + details.Add(new XElement("thumb", episode.Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot).Url)); + details.Add(new XElement("watched", "false")); +// details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault())); +// details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault())); + details.Add(new XElement("rating", episode.Ratings.Percentage)); + + //Todo: get guest stars, will need trakt to have them + + doc.Add(details); + doc.Save(xw); + + xmlResult += doc.ToString(); + xmlResult += Environment.NewLine; + } + } + + _logger.Debug("Saving episodedetails to: {0}", filename); + _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); + } + + private void WriteEpisodeImages(Series series, EpisodeFile episodeFile) + { + var screenshot = episodeFile.Episodes.Value.First().Images.Single(i => i.CoverType == MediaCoverTypes.Screenshot); + var filename = Path.ChangeExtension(episodeFile.Path, "jpg"); + + DownloadImage(series, screenshot.Url, filename); + _eventAggregator.PublishEvent(new EpisodeMetadataUpdated(series, episodeFile, GetType().Name, MetadataType.SeasonImage, DiskProvider.GetRelativePath(series.Path, filename))); + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadataSettings.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadataSettings.cs new file mode 100644 index 000000000..65b397883 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadataSettings.cs @@ -0,0 +1,57 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Metadata.Consumers.Xbmc +{ + public class XbmcSettingsValidator : AbstractValidator + { + public XbmcSettingsValidator() + { + } + } + + public class XbmcMetadataSettings : IProviderConfig + { + private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator(); + + public XbmcMetadataSettings() + { + SeriesMetadata = true; + EpisodeMetadata = true; + SeriesImages = true; + SeasonImages = true; + EpisodeImages = true; + } + + [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] + public Boolean SeriesMetadata { get; set; } + + [FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox)] + public Boolean EpisodeMetadata { get; set; } + + [FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox)] + public Boolean SeriesImages { get; set; } + + [FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox)] + public Boolean SeasonImages { get; set; } + + [FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox)] + public Boolean EpisodeImages { get; set; } + + public bool IsValid + { + get + { + return true; + } + } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Events/EpisodeMetadataUpdated.cs b/src/NzbDrone.Core/MetaData/Events/EpisodeMetadataUpdated.cs new file mode 100644 index 000000000..0b23d34b9 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Events/EpisodeMetadataUpdated.cs @@ -0,0 +1,25 @@ +using System; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Events +{ + public class EpisodeMetadataUpdated : IEvent + { + public Series Series { get; set; } + public EpisodeFile EpisodeFile { get; set; } + public String Consumer { get; set; } + public MetadataType MetadataType { get; set; } + public String Path { get; set; } + + public EpisodeMetadataUpdated(Series series, EpisodeFile episodeFile, string consumer, MetadataType metadataType, string path) + { + Series = series; + EpisodeFile = episodeFile; + Consumer = consumer; + MetadataType = metadataType; + Path = path; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Events/SeasonMetadataUpdated.cs b/src/NzbDrone.Core/MetaData/Events/SeasonMetadataUpdated.cs new file mode 100644 index 000000000..77e6f8f88 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Events/SeasonMetadataUpdated.cs @@ -0,0 +1,24 @@ +using System; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Events +{ + public class SeasonMetadataUpdated : IEvent + { + public Series Series { get; set; } + public Int32 SeasonNumber { get; set; } + public String Consumer { get; set; } + public MetadataType MetadataType { get; set; } + public String Path { get; set; } + + public SeasonMetadataUpdated(Series series, int seasonNumber, string consumer, MetadataType metadataType, string path) + { + Series = series; + SeasonNumber = seasonNumber; + Consumer = consumer; + MetadataType = metadataType; + Path = path; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Events/SeriesMetadataUpdated.cs b/src/NzbDrone.Core/MetaData/Events/SeriesMetadataUpdated.cs new file mode 100644 index 000000000..f634d1b17 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Events/SeriesMetadataUpdated.cs @@ -0,0 +1,22 @@ +using System; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Events +{ + public class SeriesMetadataUpdated : IEvent + { + public Series Series { get; set; } + public String Consumer { get; set; } + public MetadataType MetadataType { get; set; } + public String Path { get; set; } + + public SeriesMetadataUpdated(Series series, string consumer, MetadataType metadataType, string path) + { + Series = series; + Consumer = consumer; + MetadataType = metadataType; + Path = path; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/IMetadata.cs b/src/NzbDrone.Core/MetaData/IMetadata.cs new file mode 100644 index 000000000..2f56dd00c --- /dev/null +++ b/src/NzbDrone.Core/MetaData/IMetadata.cs @@ -0,0 +1,13 @@ +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata +{ + public interface IMetadata : IProvider + { + void OnSeriesUpdated(Series series); + void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); + void AfterRename(Series series); + } +} diff --git a/src/NzbDrone.Core/MetaData/MetadataConsumerBase.cs b/src/NzbDrone.Core/MetaData/MetadataConsumerBase.cs new file mode 100644 index 000000000..c11c91c10 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/MetadataConsumerBase.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata +{ + public abstract class MetadataConsumerBase : IMetadata where TSettings : IProviderConfig, new() + { + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly Logger _logger; + + protected MetadataConsumerBase(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) + { + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _logger = logger; + } + + public Type ConfigContract + { + get + { + return typeof(TSettings); + } + } + + public IEnumerable DefaultDefinitions + { + get + { + return new List(); + } + } + + public ProviderDefinition Definition { get; set; } + + public abstract void OnSeriesUpdated(Series series); + public abstract void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); + public abstract void AfterRename(Series series); + + protected TSettings Settings + { + get + { + return (TSettings)Definition.Settings; + } + } + + protected virtual void EnsureFolder(string path) + { + _diskProvider.CreateFolder(path); + } + + protected virtual void DownloadImage(Series series, string url, string path) + { + try + { + if (_diskProvider.FileExists(path)) + { + _logger.Trace("Image already exists: {0}, will not download again.", path); + return; + } + + _httpProvider.DownloadFile(url, path); + } + catch (WebException e) + { + _logger.Warn(string.Format("Couldn't download image {0} for {1}. {2}", url, series, e.Message)); + } + catch (Exception e) + { + _logger.ErrorException("Couldn't download image " + url + " for " + series, e); + } + } + + public override string ToString() + { + return GetType().Name; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/MetadataDefinition.cs b/src/NzbDrone.Core/MetaData/MetadataDefinition.cs new file mode 100644 index 000000000..c796eb8ab --- /dev/null +++ b/src/NzbDrone.Core/MetaData/MetadataDefinition.cs @@ -0,0 +1,10 @@ +using System; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Metadata +{ + public class MetadataDefinition : ProviderDefinition + { + public Boolean Enable { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetaData/MetadataFactory.cs b/src/NzbDrone.Core/MetaData/MetadataFactory.cs new file mode 100644 index 000000000..5da338813 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/MetadataFactory.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Composition; +using NzbDrone.Core.Metadata.Consumers.Fake; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Metadata +{ + public interface IMetadataFactory : IProviderFactory + { + List Enabled(); + } + + public class MetadataFactory : ProviderFactory, IMetadataFactory + { + private readonly IMetadataRepository _providerRepository; + + public MetadataFactory(IMetadataRepository providerRepository, IEnumerable providers, IContainer container, Logger logger) + : base(providerRepository, providers, container, logger) + { + _providerRepository = providerRepository; + } + + protected override void InitializeProviders() + { + var definitions = new List(); + + foreach (var provider in _providers) + { + if (provider.GetType() == typeof(FakeMetadata)) continue;; + + definitions.Add(new MetadataDefinition + { + Enable = false, + Name = provider.GetType().Name, + Implementation = provider.GetType().Name, + Settings = (IProviderConfig)Activator.CreateInstance(provider.ConfigContract) + }); + } + + var currentProviders = All(); + + var newProviders = definitions.Where(def => currentProviders.All(c => c.Implementation != def.Implementation)).ToList(); + + if (newProviders.Any()) + { + _providerRepository.InsertMany(newProviders.Cast().ToList()); + } + } + + public List Enabled() + { + return GetAvailableProviders().Where(n => ((MetadataDefinition)n.Definition).Enable).ToList(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetaData/MetadataRepository.cs b/src/NzbDrone.Core/MetaData/MetadataRepository.cs new file mode 100644 index 000000000..78b31f94f --- /dev/null +++ b/src/NzbDrone.Core/MetaData/MetadataRepository.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + + +namespace NzbDrone.Core.Metadata +{ + public interface IMetadataRepository : IProviderRepository + { + + } + + public class MetadataRepository : ProviderRepository, IMetadataRepository + { + public MetadataRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs new file mode 100644 index 000000000..c91a4689a --- /dev/null +++ b/src/NzbDrone.Core/MetaData/MetadataService.cs @@ -0,0 +1,48 @@ +using NLog; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Metadata +{ + public class NotificationService + : IHandle, + IHandle, + IHandle + { + private readonly IMetadataFactory _metadataFactory; + private readonly IMetadataRepository _metadataRepository; + private readonly Logger _logger; + + public NotificationService(IMetadataFactory metadataFactory, IMetadataRepository metadataRepository, Logger logger) + { + _metadataFactory = metadataFactory; + _metadataRepository = metadataRepository; + _logger = logger; + } + + public void Handle(SeriesUpdatedEvent message) + { + foreach (var consumer in _metadataFactory.Enabled()) + { + consumer.OnSeriesUpdated(message.Series); + } + } + + public void Handle(EpisodeImportedEvent message) + { + foreach (var consumer in _metadataFactory.Enabled()) + { + consumer.OnEpisodeImport(message.EpisodeInfo.Series, message.ImportedEpisode, message.NewDownload); + } + } + + public void Handle(SeriesRenamedEvent message) + { + foreach (var consumer in _metadataFactory.Enabled()) + { + consumer.AfterRename(message.Series); + } + } + } +} diff --git a/src/NzbDrone.Core/MetaData/MetadataType.cs b/src/NzbDrone.Core/MetaData/MetadataType.cs new file mode 100644 index 000000000..45470ffd3 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/MetadataType.cs @@ -0,0 +1,13 @@ +using System; + +namespace NzbDrone.Core.Metadata +{ + public enum MetadataType + { + SeriesMetadata = 0, + EpisodeMetadata = 1, + SeriesImage = 2, + SeasonImage = 3, + EpisodeImage = 4 + } +} diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/Actor.cs b/src/NzbDrone.Core/MetadataSource/Trakt/Actor.cs new file mode 100644 index 000000000..cc16310e2 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/Trakt/Actor.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.Trakt +{ + public class Actor + { + public string name { get; set; } + public string character { get; set; } + public Images images { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/Episode.cs b/src/NzbDrone.Core/MetadataSource/Trakt/Episode.cs index 7045f31cb..302fb49ef 100644 --- a/src/NzbDrone.Core/MetadataSource/Trakt/Episode.cs +++ b/src/NzbDrone.Core/MetadataSource/Trakt/Episode.cs @@ -13,5 +13,7 @@ public int first_aired_utc { get; set; } public string url { get; set; } public string screen { get; set; } + public Ratings ratings { get; set; } + public Images images { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/FullShow.cs b/src/NzbDrone.Core/MetadataSource/Trakt/FullShow.cs index c35ceee3f..d94675b26 100644 --- a/src/NzbDrone.Core/MetadataSource/Trakt/FullShow.cs +++ b/src/NzbDrone.Core/MetadataSource/Trakt/FullShow.cs @@ -29,6 +29,8 @@ namespace NzbDrone.Core.MetadataSource.Trakt public Images images { get; set; } public List genres { get; set; } public List seasons { get; set; } + public Ratings ratings { get; set; } + public People people { get; set; } } public class SearchShow diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/Images.cs b/src/NzbDrone.Core/MetadataSource/Trakt/Images.cs index f1e791132..aa8b15374 100644 --- a/src/NzbDrone.Core/MetadataSource/Trakt/Images.cs +++ b/src/NzbDrone.Core/MetadataSource/Trakt/Images.cs @@ -5,5 +5,7 @@ public string poster { get; set; } public string fanart { get; set; } public string banner { get; set; } + public string screen { get; set; } + public string headshot { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/People.cs b/src/NzbDrone.Core/MetadataSource/Trakt/People.cs new file mode 100644 index 000000000..31d736178 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/Trakt/People.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.Trakt +{ + public class People + { + public List actors { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/Ratings.cs b/src/NzbDrone.Core/MetadataSource/Trakt/Ratings.cs new file mode 100644 index 000000000..6302071eb --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/Trakt/Ratings.cs @@ -0,0 +1,12 @@ +using System; + +namespace NzbDrone.Core.MetadataSource.Trakt +{ + public class Ratings + { + public Int32 percentage { get; set; } + public Int32 votes { get; set; } + public Int32 loved { get; set; } + public Int32 hated { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/Trakt/Season.cs b/src/NzbDrone.Core/MetadataSource/Trakt/Season.cs index ac741b5c2..ac9bad579 100644 --- a/src/NzbDrone.Core/MetadataSource/Trakt/Season.cs +++ b/src/NzbDrone.Core/MetadataSource/Trakt/Season.cs @@ -8,5 +8,6 @@ namespace NzbDrone.Core.MetadataSource.Trakt public List episodes { get; set; } public string url { get; set; } public string poster { get; set; } + public Images images { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index aa9391f7d..aa2d133c7 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -8,7 +8,9 @@ using NLog; using NzbDrone.Common; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.Trakt; +using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Tv; +using Omu.ValueInjecter; using RestSharp; using Episode = NzbDrone.Core.Tv.Episode; using NzbDrone.Core.Rest; @@ -79,15 +81,16 @@ namespace NzbDrone.Core.MetadataSource series.AirTime = show.air_time_utc; series.TitleSlug = show.url.ToLower().Replace("http://trakt.tv/show/", ""); series.Status = GetSeriesStatus(show.status, show.ended); - - series.Seasons = show.seasons.Select(s => new Tv.Season - { - SeasonNumber = s.season - }).OrderByDescending(s => s.SeasonNumber).ToList(); + series.Ratings = GetRatings(show.ratings); + series.Genres = show.genres; + series.Certification = show.certification; + series.Actors = GetActors(show.people); + series.Seasons = GetSeasons(show); 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; } @@ -101,6 +104,9 @@ namespace NzbDrone.Core.MetadataSource episode.Title = traktEpisode.title; episode.AirDate = FromIsoToString(traktEpisode.first_aired_iso); episode.AirDateUtc = FromIso(traktEpisode.first_aired_iso); + episode.Ratings = GetRatings(traktEpisode.ratings); + + episode.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Screenshot, traktEpisode.images.screen)); return episode; } @@ -175,5 +181,60 @@ namespace NzbDrone.Core.MetadataSource return year; } + + private static Tv.Ratings GetRatings(Trakt.Ratings ratings) + { + return new Tv.Ratings + { + Percentage = ratings.percentage, + Votes = ratings.votes, + Loved = ratings.loved, + Hated = ratings.hated + }; + } + + private static List GetActors(People people) + { + if (people == null) + { + return new List(); + } + + return GetActors(people.actors).ToList(); + } + + private static IEnumerable GetActors(IEnumerable trakcActors) + { + foreach (var traktActor in trakcActors) + { + var actor = new Tv.Actor + { + Name = traktActor.name, + Character = traktActor.character, + }; + + actor.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Headshot, traktActor.images.headshot)); + + yield return actor; + } + } + + private static List GetSeasons(Show show) + { + var seasons = new List(); + + foreach (var traktSeason in show.seasons.OrderByDescending(s => s.season)) + { + var season = new Tv.Season + { + SeasonNumber = traktSeason.season + }; + + season.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Poster, traktSeason.images.poster)); + seasons.Add(season); + } + + return seasons; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c09e55f48..6ddfb32bd 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -195,6 +195,8 @@ + + @@ -310,7 +312,24 @@ + + + + + + + + + + + + + + + + + @@ -489,6 +508,7 @@ + @@ -500,6 +520,7 @@ + @@ -675,6 +696,9 @@ + + + diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 37b450154..d9910edda 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Parser Episodes = episodes, Path = filename, ParsedEpisodeInfo = parsedEpisodeInfo, - ExistingFile = _diskProvider.IsParent(series.Path, filename) + ExistingFile = DiskProvider.IsParent(series.Path, filename) }; } diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index edc273a52..bdb30d12b 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.ThingiProvider private readonly IContainer _container; private readonly Logger _logger; - private readonly List _providers; + protected readonly List _providers; protected ProviderFactory(IProviderRepository providerRepository, IEnumerable providers, diff --git a/src/NzbDrone.Core/Tv/Actor.cs b/src/NzbDrone.Core/Tv/Actor.cs new file mode 100644 index 000000000..3612e2f78 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Actor.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Tv +{ + public class Actor : IEmbeddedDocument + { + public Actor() + { + Images = new List(); + } + + public String Name { get; set; } + public String Character { get; set; } + public List Images { get; set; } + } +} diff --git a/src/NzbDrone.Core/Tv/Episode.cs b/src/NzbDrone.Core/Tv/Episode.cs index 854dceb9c..a550709b0 100644 --- a/src/NzbDrone.Core/Tv/Episode.cs +++ b/src/NzbDrone.Core/Tv/Episode.cs @@ -1,14 +1,19 @@ using System; +using System.Collections.Generic; using Marr.Data; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Common; - namespace NzbDrone.Core.Tv { public class Episode : ModelBase { + public Episode() + { + Images = new List(); + } + public const string AIR_DATE_FORMAT = "yyyy-MM-dd"; public int SeriesId { get; set; } @@ -18,12 +23,13 @@ namespace NzbDrone.Core.Tv public string Title { get; set; } public string AirDate { get; set; } public DateTime? AirDateUtc { get; set; } - public string Overview { get; set; } public Boolean Monitored { get; set; } public Nullable AbsoluteEpisodeNumber { get; set; } public int SceneSeasonNumber { get; set; } public int SceneEpisodeNumber { get; set; } + public Ratings Ratings { get; set; } + public List Images { get; set; } public String SeriesTitle { get; private set; } diff --git a/src/NzbDrone.Core/Tv/Ratings.cs b/src/NzbDrone.Core/Tv/Ratings.cs new file mode 100644 index 000000000..ffdabf95e --- /dev/null +++ b/src/NzbDrone.Core/Tv/Ratings.cs @@ -0,0 +1,13 @@ +using System; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Tv +{ + public class Ratings : IEmbeddedDocument + { + public Int32 Percentage { get; set; } + public Int32 Votes { get; set; } + public Int32 Loved { get; set; } + public Int32 Hated { get; set; } + } +} diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs index e60658104..357926f4e 100644 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -65,6 +65,8 @@ namespace NzbDrone.Core.Tv episodeToUpdate.Overview = episode.Overview; episodeToUpdate.AirDate = episode.AirDate; episodeToUpdate.AirDateUtc = episode.AirDateUtc; + episodeToUpdate.Ratings = episode.Ratings; + episodeToUpdate.Images = episode.Images; successCount++; } diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 7400a3f43..8a1776642 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -50,6 +50,10 @@ namespace NzbDrone.Core.Tv series.Images = seriesInfo.Images; series.Network = seriesInfo.Network; series.FirstAired = seriesInfo.FirstAired; + series.Ratings = seriesInfo.Ratings; + series.Actors = seriesInfo.Actors; + series.Genres = seriesInfo.Genres; + series.Certification = seriesInfo.Certification; if (_dailySeriesService.IsDailySeries(series.TvdbId)) { diff --git a/src/NzbDrone.Core/Tv/Season.cs b/src/NzbDrone.Core/Tv/Season.cs index 5073ea2b4..9b3863f83 100644 --- a/src/NzbDrone.Core/Tv/Season.cs +++ b/src/NzbDrone.Core/Tv/Season.cs @@ -1,11 +1,18 @@ using System; +using System.Collections.Generic; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Tv { public class Season : IEmbeddedDocument { + public Season() + { + Images = new List(); + } + public int SeasonNumber { get; set; } public Boolean Monitored { get; set; } + public List Images { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 107162aaf..ace922cb1 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -13,6 +13,8 @@ namespace NzbDrone.Core.Tv public Series() { Images = new List(); + Genres = new List(); + Actors = new List(); } public int TvdbId { get; set; } @@ -35,6 +37,10 @@ namespace NzbDrone.Core.Tv public string TitleSlug { get; set; } public string Path { get; set; } public int Year { get; set; } + public Ratings Ratings { get; set; } + public List Genres { get; set; } + public List Actors { get; set; } + public String Certification { get; set; } public string RootFolderPath { get; set; }