diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index eb7d95e07..c5063a268 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -34,10 +34,7 @@ namespace NzbDrone.Common.Disk 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(); + return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); } public static bool IsParent(string parentPath, string childPath) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs index 3cac9c319..2ede2be18 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs @@ -33,13 +33,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications _localEpisode.ExistingFile = true; } - private void GivenNewFile() - { - Mocker.GetMock() - .Setup(s => s.IsParent(_localEpisode.Series.Path, _localEpisode.Path)) - .Returns(false); - } - [Test] public void should_return_true_if_file_is_under_series_folder() { @@ -62,8 +55,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_return_false_if_file_is_in_use() { - GivenNewFile(); - Mocker.GetMock() .Setup(s => s.IsFileLocked(It.IsAny())) .Returns(true); @@ -74,8 +65,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_return_true_if_file_is_not_in_use() { - GivenNewFile(); - Mocker.GetMock() .Setup(s => s.IsFileLocked(It.IsAny())) .Returns(false); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs index 8fbdd09a5..3d04204be 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs @@ -36,10 +36,6 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock() .Setup(c => c.GetEpisodeBySeries(It.IsAny())) .Returns(_episodes); - - Mocker.GetMock() - .Setup(s => s.IsParent(It.IsAny(), It.IsAny())) - .Returns(true); } private void GivenEpisodeFiles(IEnumerable episodeFiles) @@ -58,13 +54,6 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(_episodes); } - private void GivenFileIsNotInSeriesFolder() - { - Mocker.GetMock() - .Setup(s => s.IsParent(It.IsAny(), It.IsAny())) - .Returns(false); - } - [Test] public void should_skip_files_that_exist_in_disk() { @@ -118,7 +107,6 @@ namespace NzbDrone.Core.Test.MediaFiles .Build(); GivenEpisodeFiles(episodeFiles); - GivenFileIsNotInSeriesFolder(); Subject.Execute(new CleanMediaFileDb(0)); 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 deleted file mode 100644 index fb0ff975e..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/037_add_episode_file_metadata.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index b5db4eac9..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/037_add_metadata_consumers.cs +++ /dev/null @@ -1,19 +0,0 @@ -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/Migration/037_add_metadata_tables.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_metadata_tables.cs new file mode 100644 index 000000000..a3e5fe15f --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_metadata_tables.cs @@ -0,0 +1,28 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(37)] + public class add_metadata_tables : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("Metadata") + .WithColumn("Enable").AsBoolean().NotNullable() + .WithColumn("Name").AsString().NotNullable() + .WithColumn("Implementation").AsString().NotNullable() + .WithColumn("Settings").AsString().NotNullable() + .WithColumn("ConfigContract").AsString().NotNullable(); + + Create.TableForModel("MetadataFiles") + .WithColumn("SeriesId").AsInt32().NotNullable() + .WithColumn("Consumer").AsString().NotNullable() + .WithColumn("Type").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable() + .WithColumn("SeasonNumber").AsInt32().Nullable() + .WithColumn("EpisodeFileId").AsInt32().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 1aa98ced4..b5711cd18 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Metadata; +using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Notifications; using NzbDrone.Core.Organizer; using NzbDrone.Core.Qualities; @@ -37,7 +38,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("Indexers"); Mapper.Entity().RegisterModel("ScheduledTasks"); Mapper.Entity().RegisterModel("Notifications"); - Mapper.Entity().RegisterModel("MetadataConsumers"); + Mapper.Entity().RegisterModel("Metadata"); Mapper.Entity().RegisterModel("SceneMappings"); @@ -60,16 +61,13 @@ namespace NzbDrone.Core.Datastore .Relationships.AutoMapICollectionOrComplexProperties(); Mapper.Entity().RegisterModel("QualityProfiles"); - Mapper.Entity().RegisterModel("QualityDefinitions"); - Mapper.Entity().RegisterModel("Logs"); - Mapper.Entity().RegisterModel("NamingConfig"); - Mapper.Entity().MapResultSet(); - Mapper.Entity().RegisterModel("Blacklist"); + + Mapper.Entity().RegisterModel("MetadataFiles"); } private static void RegisterMappers() diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 18107b1ba..fd7af51a4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -9,6 +12,7 @@ namespace NzbDrone.Core.MediaFiles { List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + EpisodeFile FindFileByPath(string path, bool includeExtension = true); } @@ -31,5 +35,14 @@ namespace NzbDrone.Core.MediaFiles .ToList(); } + public EpisodeFile FindFileByPath(string path, bool includeExtension = true) + { + if (includeExtension) + { + return Query.SingleOrDefault(c => c.Path == path); + } + + return Query.SingleOrDefault(c => c.Path.StartsWith(Path.ChangeExtension(path, ""))); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index bef1bde40..834d4dd6c 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles List FilterExistingFiles(List files, int seriesId); EpisodeFile Get(int id); List Get(IEnumerable ids); + EpisodeFile FindByPath(string path, bool includeExtension = true); } public class MediaFileService : IMediaFileService, IHandleAsync @@ -82,6 +83,11 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.Get(ids).ToList(); } + public EpisodeFile FindByPath(string path, bool includeExtension = true) + { + return _mediaFileRepository.FindFileByPath(path, includeExtension); + } + public void HandleAsync(SeriesDeletedEvent message) { var files = GetFilesBySeries(message.Series.Id); diff --git a/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs b/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs index 23c0c1653..d91467fb2 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Fake/Fake.cs @@ -7,11 +7,12 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Metadata.Consumers.Fake { - public class FakeMetadata : MetadataConsumerBase + public class FakeMetadata : MetadataBase { private readonly IDiskProvider _diskProvider; private readonly IHttpProvider _httpProvider; @@ -25,19 +26,24 @@ namespace NzbDrone.Core.Metadata.Consumers.Fake _logger = logger; } - public override void OnSeriesUpdated(Series series) + public override void OnSeriesUpdated(Tv.Series series) { throw new NotImplementedException(); } - public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) + public override void OnEpisodeImport(Tv.Series series, EpisodeFile episodeFile, bool newDownload) { throw new NotImplementedException(); } - public override void AfterRename(Series series) + public override void AfterRename(Tv.Series series) { throw new NotImplementedException(); } + + public override MetadataFile FindMetadataFile(Series series, string path) + { + return null; + } } } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index 21ee28326..d9eb45338 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -1,8 +1,8 @@ using System; using System.IO; using System.Linq; -using System.Net; using System.Text; +using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using NLog; @@ -10,21 +10,25 @@ using NzbDrone.Common; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Metadata.Events; +using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Metadata.Consumers.Xbmc { - public class XbmcMetadata : MetadataConsumerBase + public class XbmcMetadata : MetadataBase { private readonly IEventAggregator _eventAggregator; private readonly IMapCoversToLocal _mediaCoverService; + private readonly IMediaFileService _mediaFileService; + private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; private readonly IHttpProvider _httpProvider; private readonly Logger _logger; public XbmcMetadata(IEventAggregator eventAggregator, IMapCoversToLocal mediaCoverService, + IMediaFileService mediaFileService, + IMetadataFileService metadataFileService, IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) @@ -32,11 +36,17 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc { _eventAggregator = eventAggregator; _mediaCoverService = mediaCoverService; + _mediaFileService = mediaFileService; + _metadataFileService = metadataFileService; _diskProvider = diskProvider; _httpProvider = httpProvider; _logger = logger; } + private static readonly Regex SeriesImagesRegex = new Regex(@"^(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?\d{2,}|-all|-specials)-(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public override void OnSeriesUpdated(Series series) { if (Settings.SeriesMetadata) @@ -62,7 +72,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc { if (Settings.EpisodeMetadata) { - WriteEpisodeNfo(episodeFile); + WriteEpisodeNfo(series, episodeFile); } if (Settings.EpisodeImages) @@ -73,9 +83,97 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc public override void AfterRename(Series series) { - //TODO: Rename media files to match episode files + var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); + var episodeFilesMetadata = _metadataFileService.GetFilesBySeries(series.Id).Where(c => c.EpisodeFileId > 0).ToList(); + + foreach (var episodeFile in episodeFiles) + { + var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); + var episodeFilenameWithoutExtension = + Path.GetFileNameWithoutExtension(DiskProvider.GetRelativePath(series.Path, episodeFile.Path)); + + foreach (var metadataFile in metadataFiles) + { + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(metadataFile.RelativePath); + var extension = Path.GetExtension(metadataFile.RelativePath); + + if (!fileNameWithoutExtension.Equals(episodeFilenameWithoutExtension)) + { + var source = Path.Combine(series.Path, metadataFile.RelativePath); + var destination = Path.Combine(series.Path, fileNameWithoutExtension + extension); + + _diskProvider.MoveFile(source, destination); + metadataFile.RelativePath = fileNameWithoutExtension + extension; + + _eventAggregator.PublishEvent(new MetadataFileUpdated(metadataFile)); + } + } + } + } + + public override MetadataFile FindMetadataFile(Series series, string path) + { + var filename = Path.GetFileName(path); + + if (filename == null) return null; + + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + RelativePath = DiskProvider.GetRelativePath(series.Path, path) + }; + + if (SeriesImagesRegex.IsMatch(filename)) + { + metadata.Type = MetadataType.SeriesImage; + return metadata; + } + + var seasonMatch = SeasonImagesRegex.Match(filename); + + if (seasonMatch.Success) + { + metadata.Type = MetadataType.SeasonImage; + + var seasonNumber = seasonMatch.Groups["season"].Value; + + if (seasonNumber.Contains("specials")) + { + metadata.SeasonNumber = 0; + } + + else + { + metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + } + + return metadata; + } + + if (EpisodeImageRegex.IsMatch(filename)) + { + metadata.Type = MetadataType.EpisodeImage; + return metadata; + } + + if (filename.Equals("tvshow.nfo", StringComparison.InvariantCultureIgnoreCase)) + { + metadata.Type = MetadataType.SeriesMetadata; + return metadata; + } + + var parseResult = Parser.Parser.ParseTitle(filename); + + if (parseResult != null && + !parseResult.FullSeason && + Path.GetExtension(filename) == ".nfo") + { + metadata.Type = MetadataType.EpisodeMetadata; + return metadata; + } - throw new NotImplementedException(); + return null; } private void WriteTvShowNfo(Series series) @@ -130,7 +228,15 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc _diskProvider.WriteAllText(path, doc.ToString()); - _eventAggregator.PublishEvent(new SeriesMetadataUpdated(series, GetType().Name, MetadataType.SeriesMetadata, DiskProvider.GetRelativePath(series.Path, path))); + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + Type = MetadataType.SeriesMetadata, + RelativePath = DiskProvider.GetRelativePath(series.Path, path) + }; + + _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); } } @@ -149,7 +255,16 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } _diskProvider.CopyFile(source, destination, false); - _eventAggregator.PublishEvent(new SeriesMetadataUpdated(series, GetType().Name, MetadataType.SeriesImage, DiskProvider.GetRelativePath(series.Path, destination))); + + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + Type = MetadataType.SeriesImage, + RelativePath = DiskProvider.GetRelativePath(series.Path, destination) + }; + + _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); } } @@ -169,12 +284,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc 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))); + + var metadata = new MetadataFile + { + SeriesId = series.Id, + SeasonNumber = season.SeasonNumber, + Consumer = GetType().Name, + Type = MetadataType.SeasonImage, + RelativePath = DiskProvider.GetRelativePath(series.Path, path) + }; + + _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); } } } - private void WriteEpisodeNfo(EpisodeFile episodeFile) + private void WriteEpisodeNfo(Series series, EpisodeFile episodeFile) { var filename = episodeFile.Path.Replace(Path.GetExtension(episodeFile.Path), ".nfo"); @@ -218,15 +343,37 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc _logger.Debug("Saving episodedetails to: {0}", filename); _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); + + var metadata = new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.SeasonImage, + RelativePath = DiskProvider.GetRelativePath(series.Path, filename) + }; + + _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); } 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"); + + var filename = Path.GetFileNameWithoutExtension(episodeFile.Path) + "-thumb.jpg"; DownloadImage(series, screenshot.Url, filename); - _eventAggregator.PublishEvent(new EpisodeMetadataUpdated(series, episodeFile, GetType().Name, MetadataType.SeasonImage, DiskProvider.GetRelativePath(series.Path, filename))); + + var metadata = new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.SeasonImage, + RelativePath = DiskProvider.GetRelativePath(series.Path, filename) + }; + + _eventAggregator.PublishEvent(new MetadataFileUpdated(metadata)); } } } diff --git a/src/NzbDrone.Core/MetaData/Events/EpisodeMetadataUpdated.cs b/src/NzbDrone.Core/MetaData/Events/EpisodeMetadataUpdated.cs deleted file mode 100644 index 0b23d34b9..000000000 --- a/src/NzbDrone.Core/MetaData/Events/EpisodeMetadataUpdated.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 77e6f8f88..000000000 --- a/src/NzbDrone.Core/MetaData/Events/SeasonMetadataUpdated.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index f634d1b17..000000000 --- a/src/NzbDrone.Core/MetaData/Events/SeriesMetadataUpdated.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 index 2f56dd00c..2e606ac4a 100644 --- a/src/NzbDrone.Core/MetaData/IMetadata.cs +++ b/src/NzbDrone.Core/MetaData/IMetadata.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; @@ -9,5 +10,6 @@ namespace NzbDrone.Core.Metadata void OnSeriesUpdated(Series series); void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); void AfterRename(Series series); + MetadataFile FindMetadataFile(Series series, string path); } } diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs index c91a4689a..822cd2cf8 100644 --- a/src/NzbDrone.Core/MetaData/MetadataService.cs +++ b/src/NzbDrone.Core/MetaData/MetadataService.cs @@ -1,4 +1,6 @@ -using NLog; +using System.IO; +using NLog; +using NzbDrone.Common; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -11,13 +13,11 @@ namespace NzbDrone.Core.Metadata IHandle { private readonly IMetadataFactory _metadataFactory; - private readonly IMetadataRepository _metadataRepository; private readonly Logger _logger; - public NotificationService(IMetadataFactory metadataFactory, IMetadataRepository metadataRepository, Logger logger) + public NotificationService(IMetadataFactory metadataFactory, Logger logger) { _metadataFactory = metadataFactory; - _metadataRepository = metadataRepository; _logger = logger; } diff --git a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs new file mode 100644 index 000000000..e8bf85e0a --- /dev/null +++ b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Metadata +{ + public class ExistingMetadataService : IHandleAsync + { + private readonly IDiskProvider _diskProvider; + private readonly IMetadataFileService _metadataFileService; + private readonly IMediaFileService _mediaFileService; + private readonly Logger _logger; + private readonly List _consumers; + + public ExistingMetadataService(IDiskProvider diskProvider, + IEnumerable consumers, + IMetadataFileService metadataFileService, + IMediaFileService mediaFileService, + Logger logger) + { + _diskProvider = diskProvider; + _metadataFileService = metadataFileService; + _mediaFileService = mediaFileService; + _logger = logger; + _consumers = consumers.ToList(); + } + + public void HandleAsync(SeriesUpdatedEvent message) + { + if (!_diskProvider.FolderExists(message.Series.Path)) return; + + _logger.Trace("Looking for existing metadata in {0}", message.Series.Path); + + var filesOnDisk = _diskProvider.GetFiles(message.Series.Path, SearchOption.AllDirectories); + var possibleMetadataFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower())).ToList(); + var filteredFiles = _metadataFileService.FilterExistingFiles(possibleMetadataFiles, message.Series); + + foreach (var possibleMetadataFile in filteredFiles) + { + foreach (var consumer in _consumers) + { + var metadata = consumer.FindMetadataFile(message.Series, possibleMetadataFile); + + if (metadata == null) continue; + + if (metadata.Type == MetadataType.EpisodeImage || + metadata.Type == MetadataType.EpisodeMetadata) + { + //TODO: replace this with parser lookup, otherwise its impossible to link thumbs without knowing too much about the consumers + //We might want to resort to parsing the file name and + //then finding it via episodes incase the file names get out of sync + var episodeFile = _mediaFileService.FindByPath(possibleMetadataFile, false); + + if (episodeFile == null) break; + + metadata.EpisodeFileId = episodeFile.Id; + } + + _metadataFileService.Upsert(metadata); + } + } + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs new file mode 100644 index 000000000..df52e2bc4 --- /dev/null +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs @@ -0,0 +1,16 @@ +using System; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Metadata.Files +{ + public class MetadataFile : ModelBase + { + public Int32 SeriesId { get; set; } + public String Consumer { get; set; } + public MetadataType Type { get; set; } + public String RelativePath { get; set; } + public DateTime LastUpdated { get; set; } + public Int32? EpisodeFileId { get; set; } + public Int32? SeasonNumber { get; set; } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs new file mode 100644 index 000000000..38889fbb3 --- /dev/null +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Metadata.Files +{ + public interface IMetadataFileRepository : IBasicRepository + { + void DeleteForSeries(int seriesId); + void DeleteForSeason(int seriesId, int seasonNumber); + void DeleteForEpisodeFile(int episodeFileId); + List GetFilesBySeries(int seriesId); + List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFilesByEpisodeFile(int episodeFileId); + MetadataFile FindByPath(string path); + } + + public class MetadataFileRepository : BasicRepository, IMetadataFileRepository + { + public MetadataFileRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public void DeleteForSeries(int seriesId) + { + Delete(c => c.SeriesId == seriesId); + } + + public void DeleteForSeason(int seriesId, int seasonNumber) + { + Delete(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); + } + + public void DeleteForEpisodeFile(int episodeFileId) + { + Delete(c => c.EpisodeFileId == episodeFileId); + } + + public List GetFilesBySeries(int seriesId) + { + return Query.Where(c => c.SeriesId == seriesId); + } + + public List GetFilesBySeason(int seriesId, int seasonNumber) + { + return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); + } + + public List GetFilesByEpisodeFile(int episodeFileId) + { + return Query.Where(c => c.EpisodeFileId == episodeFileId); + } + + public MetadataFile FindByPath(string path) + { + return Query.SingleOrDefault(c => c.RelativePath == path); + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs new file mode 100644 index 000000000..37177a8d1 --- /dev/null +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Metadata.Files +{ + public interface IMetadataFileService + { + List GetFilesBySeries(int seriesId); + List GetFilesByEpisodeFile(int episodeFileId); + MetadataFile FindByPath(string path); + List FilterExistingFiles(List files, Series series); + MetadataFile Upsert(MetadataFile metadataFile); + } + + public class MetadataFileService : IMetadataFileService, + IHandleAsync, + IHandleAsync, + IHandle + { + private readonly IMetadataFileRepository _repository; + private readonly ISeriesService _seriesService; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public MetadataFileService(IMetadataFileRepository repository, + ISeriesService seriesService, + IDiskProvider diskProvider, + Logger logger) + { + _repository = repository; + _seriesService = seriesService; + _diskProvider = diskProvider; + _logger = logger; + } + + public List GetFilesBySeries(int seriesId) + { + return _repository.GetFilesBySeries(seriesId); + } + + public List GetFilesByEpisodeFile(int episodeFileId) + { + return _repository.GetFilesByEpisodeFile(episodeFileId); + } + + public MetadataFile FindByPath(string path) + { + return _repository.FindByPath(path); + } + + public List FilterExistingFiles(List files, Series series) + { + var seriesFiles = GetFilesBySeries(series.Id).Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(); + + if (!seriesFiles.Any()) return files; + + return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); + } + + public MetadataFile Upsert(MetadataFile metadataFile) + { + metadataFile.LastUpdated = DateTime.UtcNow; + return _repository.Upsert(metadataFile); + } + + public void HandleAsync(SeriesDeletedEvent message) + { + _logger.Trace("Deleting Metadata from database for series: {0}", message.Series); + _repository.DeleteForSeries(message.Series.Id); + } + + public void HandleAsync(EpisodeFileDeletedEvent message) + { + var episodeFile = message.EpisodeFile; + var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId); + + foreach (var metadata in _repository.GetFilesByEpisodeFile(episodeFile.Id)) + { + var path = Path.Combine(series.Path, metadata.RelativePath); + + if (_diskProvider.FileExists(path)) + { + _diskProvider.DeleteFile(path); + } + } + + _logger.Trace("Deleting Metadata from database for episode file: {0}", episodeFile); + _repository.DeleteForEpisodeFile(episodeFile.Id); + } + + public void Handle(MetadataFileUpdated message) + { + Upsert(message.Metadata); + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileUpdated.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileUpdated.cs new file mode 100644 index 000000000..7f7b4b189 --- /dev/null +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFileUpdated.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Metadata.Files +{ + public class MetadataFileUpdated : IEvent + { + public MetadataFile Metadata { get; set; } + + public MetadataFileUpdated(MetadataFile metadata) + { + Metadata = metadata; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/MetadataConsumerBase.cs b/src/NzbDrone.Core/Metadata/MetadataBase.cs similarity index 87% rename from src/NzbDrone.Core/MetaData/MetadataConsumerBase.cs rename to src/NzbDrone.Core/Metadata/MetadataBase.cs index c11c91c10..9b8a454f1 100644 --- a/src/NzbDrone.Core/MetaData/MetadataConsumerBase.cs +++ b/src/NzbDrone.Core/Metadata/MetadataBase.cs @@ -1,22 +1,22 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net; using NLog; using NzbDrone.Common; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Metadata { - public abstract class MetadataConsumerBase : IMetadata where TSettings : IProviderConfig, new() + public abstract class MetadataBase : 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) + protected MetadataBase(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) { _diskProvider = diskProvider; _httpProvider = httpProvider; @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Metadata public abstract void OnSeriesUpdated(Series series); public abstract void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); public abstract void AfterRename(Series series); + public abstract MetadataFile FindMetadataFile(Series series, string path); protected TSettings Settings { diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index aa2d133c7..a731a0c86 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -230,7 +230,11 @@ namespace NzbDrone.Core.MetadataSource SeasonNumber = traktSeason.season }; - season.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Poster, traktSeason.images.poster)); + if (traktSeason.images != null) + { + season.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Poster, traktSeason.images.poster)); + } + seasons.Add(season); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 6ddfb32bd..404ede750 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -196,7 +196,7 @@ - + @@ -319,11 +319,13 @@ - - - + + + + + - + @@ -696,9 +698,6 @@ - - -