diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index 5cfe4944f..1678cb0d6 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -6,10 +6,16 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras { - public class ExistingExtraFileService : IHandle + public interface IExistingExtraFiles : IHandle + { + List ImportFileList(Series series, List possibleExtraFiles); + } + + public class ExistingExtraFileService : IExistingExtraFiles { private readonly IDiskProvider _diskProvider; private readonly IDiskScanService _diskScanService; @@ -27,20 +33,10 @@ namespace NzbDrone.Core.Extras _logger = logger; } - public void Handle(SeriesScannedEvent message) + public List ImportFileList(Series series, List possibleExtraFiles) { - var series = message.Series; - - if (!_diskProvider.FolderExists(series.Path)) - { - return; - } - _logger.Debug("Looking for existing extra files in {0}", series.Path); - var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path); - var possibleExtraFiles = _diskScanService.FilterPaths(series.Path, filesOnDisk); - var importedFiles = new List(); foreach (var existingExtraFileImporter in _existingExtraFileImporters) @@ -50,6 +46,23 @@ namespace NzbDrone.Core.Extras importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath))); } + return importedFiles; + } + + public void Handle(SeriesScannedEvent message) + { + var series = message.Series; + + if (!_diskProvider.FolderExists(series.Path)) + { + return; + } + + var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path); + var possibleExtraFiles = _diskScanService.FilterPaths(series.Path, filesOnDisk); + + var importedFiles = ImportFileList(series, possibleExtraFiles); + _logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count); } } diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index eca970898..b29fa6d17 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Extras { public interface IExtraService { + void MoveFilesAfterRename(Series series, EpisodeFile episodeFile); void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); } @@ -139,6 +140,16 @@ namespace NzbDrone.Core.Extras } } + public void MoveFilesAfterRename(Series series, EpisodeFile episodeFile) + { + var episodeFiles = new List { episodeFile }; + + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.MoveFilesAfterRename(series, episodeFiles); + } + } + public void Handle(SeriesRenamedEvent message) { var series = message.Series; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 3ea8a9fc1..958eed347 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -130,6 +130,7 @@ namespace NzbDrone.Core.MediaFiles try { MoveEpisodeFile(episodeFile, series, episodeFile.Episodes); + localEpisode.ImportRenamed = true; } catch (SameFilenameException) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index a77cc8957..e956d7718 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IUpgradeMediaFiles _episodeFileUpgrader; private readonly IMediaFileService _mediaFileService; private readonly IExtraService _extraService; + private readonly IExistingExtraFiles _existingExtraFiles; private readonly IDiskProvider _diskProvider; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _commandQueueManager; @@ -34,6 +35,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader, IMediaFileService mediaFileService, IExtraService extraService, + IExistingExtraFiles existingExtraFiles, IDiskProvider diskProvider, IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager, @@ -42,6 +44,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _episodeFileUpgrader = episodeFileUpgrader; _mediaFileService = mediaFileService; _extraService = extraService; + _existingExtraFiles = existingExtraFiles; _diskProvider = diskProvider; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; @@ -130,7 +133,19 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (newDownload) { - _extraService.ImportEpisode(localEpisode, episodeFile, copyOnly); + if (localEpisode.ScriptImported) + { + _existingExtraFiles.ImportFileList(localEpisode.Series, localEpisode.PossibleExtraFiles); + + if (localEpisode.ImportRenamed) + { + _extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile); + } + } + else + { + _extraService.ImportEpisode(localEpisode, episodeFile, copyOnly); + } } _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, oldFiles, newDownload, downloadClientItem)); diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs index c626d5b44..a80667a2b 100644 --- a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs +++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; @@ -6,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Processes; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -25,6 +27,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IProcessProvider _processProvider; private readonly IConfigService _configService; private readonly ITagRepository _tagRepository; + private readonly IExistingExtraFiles _existingExtraFiles; private readonly Logger _logger; public ImportScriptService(IProcessProvider processProvider, @@ -32,6 +35,7 @@ namespace NzbDrone.Core.MediaFiles IConfigService configService, IConfigFileProvider configFileProvider, ITagRepository tagRepository, + IExistingExtraFiles existingExtraFiles, Logger logger) { _processProvider = processProvider; @@ -39,9 +43,63 @@ namespace NzbDrone.Core.MediaFiles _configService = configService; _configFileProvider = configFileProvider; _tagRepository = tagRepository; + _existingExtraFiles = existingExtraFiles; _logger = logger; } + private (List possibleMediaFiles, string mediaFile) ProcessTemporaryFile(string temporaryPath) + { + var lines = File.ReadAllLines(temporaryPath); + + if (lines.Length == 0 || string.IsNullOrWhiteSpace(lines[0])) + { + return (new List(), null); + } + + var possibleExtraFiles = new List(); + string mediaFile = null; + + foreach (var line in lines) + { + if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(line))) + { + if (mediaFile is not null) + { + throw new ScriptImportException("Script output contains multiple media files. Only one media file can be returned."); + } + else + { + if (File.Exists(line)) + { + mediaFile = line; + } + else + { + _logger.Warn("Script output contains invalid file: {0}", line); + } + } + } + else + { + if (File.Exists(line)) + { + possibleExtraFiles.Add(line); + } + else + { + _logger.Warn("Script output contains invalid file: {0}", line); + } + } + } + + if (mediaFile is not null) + { + throw new ScriptImportException("Script output does not contain a media file."); + } + + return (possibleExtraFiles, mediaFile); + } + public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalEpisode localEpisode, EpisodeFile episodeFile, TransferMode mode) { var series = localEpisode.Series; @@ -54,10 +112,13 @@ namespace NzbDrone.Core.MediaFiles return ScriptImportDecision.DeferMove; } + var temporaryPath = Path.GetTempFileName(); + var environmentVariables = new StringDictionary(); environmentVariables.Add("Sonarr_SourcePath", sourcePath); environmentVariables.Add("Sonarr_DestinationPath", destinationFilePath); + environmentVariables.Add("Sonarr_InfoFilePath", temporaryPath); environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); @@ -118,11 +179,21 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Executed external script: {0} - Status: {1}", _configService.ScriptImportPath, processOutput.ExitCode); _logger.Debug("Script Output: \r\n{0}", string.Join("\r\n", processOutput.Lines)); + var (possibleExtraFiles, mediaFile) = ProcessTemporaryFile(temporaryPath); + + localEpisode.PossibleExtraFiles = possibleExtraFiles; + + destinationFilePath = mediaFile ?? destinationFilePath; + episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath); + episodeFile.Path = destinationFilePath; + switch (processOutput.ExitCode) { case 0: // Copy complete + localEpisode.ScriptImported = true; return ScriptImportDecision.MoveComplete; case 2: // Copy complete, file potentially changed, should try renaming again + localEpisode.ScriptImported = true; episodeFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(destinationFilePath); episodeFile.Path = null; return ScriptImportDecision.RenameRequested; diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index bcb9a5132..8c595f8b4 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -40,6 +40,9 @@ namespace NzbDrone.Core.Parser.Model public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } public GrabbedReleaseInfo Release { get; set; } + public bool ScriptImported { get; set; } + public bool ImportRenamed { get; set; } + public List PossibleExtraFiles { get; set; } public int SeasonNumber {