diff --git a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs new file mode 100644 index 000000000..6f7043fec --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs @@ -0,0 +1,65 @@ +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Xbmc; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Xbmc +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_metadata_for_xbmc_nfo() + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode.nfo"); + + Mocker.GetMock() + .Setup(v => v.IsXbmcNfoFile(path)) + .Returns(true); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.EpisodeMetadata); + + Mocker.GetMock() + .Verify(v => v.IsXbmcNfoFile(It.IsAny()), Times.Once()); + } + + [Test] + public void should_return_null_for_scene_nfo() + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode.nfo"); + + Mocker.GetMock() + .Setup(v => v.IsXbmcNfoFile(path)) + .Returns(false); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + + Mocker.GetMock() + .Verify(v => v.IsXbmcNfoFile(It.IsAny()), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index b2d7a3b0f..d29f8bb25 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -220,6 +220,7 @@ Always + @@ -340,8 +341,8 @@ - - + + diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 99d9ec3a6..ce5b660b6 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Extras { public interface IExtraService { - void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); + void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); } public class ExtraService : IExtraService, @@ -48,15 +48,15 @@ namespace NzbDrone.Core.Extras _logger = logger; } - public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) + public void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) { - var series = localEpisode.Series; - - foreach (var extraFileManager in _extraFileManagers) - { - extraFileManager.CreateAfterEpisodeImport(series, episodeFile); - } + ImportExtraFiles(localEpisode, episodeFile, isReadOnly); + + CreateAfterImport(localEpisode.Series, episodeFile); + } + private void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) + { if (!_configService.ImportExtraFiles) { return; @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Extras foreach (var extraFileManager in _extraFileManagers) { var extension = Path.GetExtension(matchingFilename); - var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, extension, isReadOnly); + var extraFile = extraFileManager.Import(localEpisode.Series, episodeFile, matchingFilename, extension, isReadOnly); if (extraFile != null) { @@ -102,6 +102,14 @@ namespace NzbDrone.Core.Extras } } + private void CreateAfterImport(Series series, EpisodeFile episodeFile) + { + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.CreateAfterEpisodeImport(series, episodeFile); + } + } + public void Handle(MediaCoversUpdatedEvent message) { var series = message.Series; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 6cfd5f2d0..096d7c889 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -18,14 +18,17 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { public class XbmcMetadata : MetadataBase { - private readonly IMapCoversToLocal _mediaCoverService; private readonly Logger _logger; + private readonly IMapCoversToLocal _mediaCoverService; + private readonly IDetectXbmcNfo _detectNfo; - public XbmcMetadata(IMapCoversToLocal mediaCoverService, + public XbmcMetadata(IDetectXbmcNfo detectNfo, + IMapCoversToLocal mediaCoverService, Logger logger) { - _mediaCoverService = mediaCoverService; _logger = logger; + _mediaCoverService = mediaCoverService; + _detectNfo = detectNfo; } private static readonly Regex SeriesImagesRegex = new Regex(@"^(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -114,7 +117,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc if (parseResult != null && !parseResult.FullSeason && - Path.GetExtension(filename).Equals(".nfo", StringComparison.OrdinalIgnoreCase)) + Path.GetExtension(filename).Equals(".nfo", StringComparison.OrdinalIgnoreCase) && + _detectNfo.IsXbmcNfoFile(path)) { metadata.Type = MetadataType.EpisodeMetadata; return metadata; @@ -294,6 +298,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc } } + + RenameExistingNfo(GetEpisodeMetadataFilename(episodeFile.RelativePath)); + return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); } @@ -373,6 +380,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc } } + private void RenameExistingNfo(string nfoFilePath) + { + + } + private string GetEpisodeMetadataFilename(string episodeFilePath) { return Path.ChangeExtension(episodeFilePath, "nfo"); diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs new file mode 100644 index 000000000..405d0e7a3 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc +{ + public interface IDetectXbmcNfo + { + bool IsXbmcNfoFile(string path); + } + + public class XbmcNfoDetector : IDetectXbmcNfo + { + private readonly IDiskProvider _diskProvider; + + private readonly Regex _regex = new Regex("<(movie|tvshow|episodedetails|artist|album|musicvideo)>", RegexOptions.Compiled); + + public XbmcNfoDetector(IDiskProvider diskProvider) + { + _diskProvider = diskProvider; + } + + public bool IsXbmcNfoFile(string path) + { + // Lets make sure we're not reading huge files. + if (_diskProvider.GetFileSize(path) > 10.Megabytes()) + { + return false; + } + + // Check if it contains some of the kodi/xbmc xml tags + var content = _diskProvider.ReadAllText(path); + + return _regex.IsMatch(content); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 4731655e7..793662bcd 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; +using NzbDrone.Core.Extras.Others; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -20,6 +21,7 @@ namespace NzbDrone.Core.Extras.Metadata private readonly IMetadataFactory _metadataFactory; private readonly ICleanMetadataService _cleanMetadataService; private readonly IRecycleBinProvider _recycleBinProvider; + private readonly IOtherExtraFileRenamer _otherExtraFileRenamer; private readonly IDiskTransferService _diskTransferService; private readonly IDiskProvider _diskProvider; private readonly IHttpClient _httpClient; @@ -31,6 +33,7 @@ namespace NzbDrone.Core.Extras.Metadata IDiskProvider diskProvider, IDiskTransferService diskTransferService, IRecycleBinProvider recycleBinProvider, + IOtherExtraFileRenamer otherExtraFileRenamer, IMetadataFactory metadataFactory, ICleanMetadataService cleanMetadataService, IHttpClient httpClient, @@ -41,6 +44,7 @@ namespace NzbDrone.Core.Extras.Metadata { _metadataFactory = metadataFactory; _cleanMetadataService = cleanMetadataService; + _otherExtraFileRenamer = otherExtraFileRenamer; _recycleBinProvider = recycleBinProvider; _diskTransferService = diskTransferService; _diskProvider = diskProvider; @@ -91,7 +95,6 @@ namespace NzbDrone.Core.Extras.Metadata foreach (var consumer in _metadataFactory.Enabled()) { - files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List())); files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List())); } @@ -238,6 +241,8 @@ namespace NzbDrone.Core.Extras.Metadata var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath); + _otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath); + var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeMetadata && c.EpisodeFileId == episodeFile.Id); @@ -292,6 +297,8 @@ namespace NzbDrone.Core.Extras.Metadata continue; } + _otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath); + var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage && c.RelativePath == image.RelativePath) ?? new MetadataFile @@ -327,6 +334,8 @@ namespace NzbDrone.Core.Extras.Metadata continue; } + _otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath); + var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeasonImage && c.SeasonNumber == season.SeasonNumber && c.RelativePath == image.RelativePath) ?? @@ -363,6 +372,8 @@ namespace NzbDrone.Core.Extras.Metadata continue; } + _otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath); + var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage && c.EpisodeFileId == episodeFile.Id); diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs new file mode 100644 index 000000000..ff4df604f --- /dev/null +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Others +{ + public interface IOtherExtraFileRenamer + { + void RenameOtherExtraFile(Series series, string path); + } + + public class OtherExtraFileRenamer : IOtherExtraFileRenamer + { + private readonly Logger _logger; + private readonly IDiskProvider _diskProvider; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly ISeriesService _seriesService; + private readonly IOtherExtraFileService _otherExtraFileService; + + public OtherExtraFileRenamer(IOtherExtraFileService otherExtraFileService, + ISeriesService seriesService, + IRecycleBinProvider recycleBinProvider, + IDiskProvider diskProvider, + Logger logger) + { + _logger = logger; + _diskProvider = diskProvider; + _recycleBinProvider = recycleBinProvider; + _seriesService = seriesService; + _otherExtraFileService = otherExtraFileService; + } + + public void RenameOtherExtraFile(Series series, string path) + { + if (!_diskProvider.FileExists(path)) + { + return; + } + + var relativePath = series.Path.GetRelativePath(path); + + var otherExtraFile = _otherExtraFileService.FindByPath(relativePath); + if (otherExtraFile != null) + { + var newPath = path + "-orig"; + + // Recycle an existing -orig file. + RemoveOtherExtraFile(series, newPath); + + // Rename the file to .*-orig + _diskProvider.MoveFile(path, newPath); + otherExtraFile.RelativePath = relativePath + "-orig"; + otherExtraFile.Extension += "-orig"; + _otherExtraFileService.Upsert(otherExtraFile); + } + } + + private void RemoveOtherExtraFile(Series series, string path) + { + if (!_diskProvider.FileExists(path)) + { + return; + } + + var relativePath = series.Path.GetRelativePath(path); + + var otherExtraFile = _otherExtraFileService.FindByPath(relativePath); + if (otherExtraFile != null) + { + var subfolder = Path.GetDirectoryName(relativePath); + _recycleBinProvider.DeleteFile(path, subfolder); + } + } + } +} diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index 18370b0fe..62d9f5129 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -65,12 +65,6 @@ namespace NzbDrone.Core.Extras.Others public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) { - // If the extension is .nfo we need to change it to .nfo-orig - if (Path.GetExtension(path).Equals(".nfo", StringComparison.OrdinalIgnoreCase)) - { - extension += "-orig"; - } - var extraFile = ImportFile(series, episodeFile, path, readOnly, extension, null); _otherExtraFileService.Upsert(extraFile); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 7c281c65a..2a50ff2bf 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -117,7 +117,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (newDownload) { - _extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly); + _extraService.ImportEpisode(localEpisode, episodeFile, copyOnly); } _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, oldFiles, newDownload, downloadClientItem)); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c553d7ff2..eaafa418d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -554,6 +554,7 @@ + @@ -562,6 +563,7 @@ +