1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-12 11:15:43 +02:00

New: Custom import scripts can communicate information back

This commit is contained in:
Jendrik Weise 2023-11-19 19:52:37 +01:00 committed by GitHub
parent 3541cd7ba8
commit b4ac495983
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 47 deletions

View File

@ -267,7 +267,7 @@ public void should_not_scan_extras_subfolder()
Subject.Scan(_series); Subject.Scan(_series);
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<bool>()), Times.Once()); .Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<bool>()), Times.Exactly(2));
Mocker.GetMock<IMakeImportDecision>() Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series, false), Times.Once()); .Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series, false), Times.Once());

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
@ -71,7 +72,7 @@ public void should_skip_up_to_date_media_info()
GivenFileExists(); GivenFileExists();
GivenSuccessfulScan(); GivenSuccessfulScan();
Subject.Handle(new SeriesScannedEvent(_series)); Subject.Handle(new SeriesScannedEvent(_series, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>() Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2)); .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2));
@ -97,7 +98,7 @@ public void should_skip_not_yet_date_media_info()
GivenFileExists(); GivenFileExists();
GivenSuccessfulScan(); GivenSuccessfulScan();
Subject.Handle(new SeriesScannedEvent(_series)); Subject.Handle(new SeriesScannedEvent(_series, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>() Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2)); .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2));
@ -123,7 +124,7 @@ public void should_update_outdated_media_info()
GivenFileExists(); GivenFileExists();
GivenSuccessfulScan(); GivenSuccessfulScan();
Subject.Handle(new SeriesScannedEvent(_series)); Subject.Handle(new SeriesScannedEvent(_series, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>() Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(3)); .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(3));
@ -146,7 +147,7 @@ public void should_ignore_missing_files()
GivenSuccessfulScan(); GivenSuccessfulScan();
Subject.Handle(new SeriesScannedEvent(_series)); Subject.Handle(new SeriesScannedEvent(_series, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>() Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo("media.mkv"), Times.Never()); .Verify(v => v.GetMediaInfo("media.mkv"), Times.Never());
@ -173,7 +174,7 @@ public void should_continue_after_failure()
GivenSuccessfulScan(); GivenSuccessfulScan();
GivenFailedScan(Path.Combine(_series.Path, "media2.mkv")); GivenFailedScan(Path.Combine(_series.Path, "media2.mkv"));
Subject.Handle(new SeriesScannedEvent(_series)); Subject.Handle(new SeriesScannedEvent(_series, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>() Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(1)); .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(1));
@ -203,7 +204,7 @@ public void should_not_update_files_if_media_info_disabled()
GivenFileExists(); GivenFileExists();
GivenSuccessfulScan(); GivenSuccessfulScan();
Subject.Handle(new SeriesScannedEvent(_series)); Subject.Handle(new SeriesScannedEvent(_series, new List<string>()));
Mocker.GetMock<IVideoFileInfoReader>() Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(It.IsAny<string>()), Times.Never()); .Verify(v => v.GetMediaInfo(It.IsAny<string>()), Times.Never());

View File

@ -2,45 +2,33 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras namespace NzbDrone.Core.Extras
{ {
public class ExistingExtraFileService : IHandle<SeriesScannedEvent> public interface IExistingExtraFiles
{
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles);
}
public class ExistingExtraFileService : IExistingExtraFiles, IHandle<SeriesScannedEvent>
{ {
private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService;
private readonly List<IImportExistingExtraFiles> _existingExtraFileImporters; private readonly List<IImportExistingExtraFiles> _existingExtraFileImporters;
private readonly Logger _logger; private readonly Logger _logger;
public ExistingExtraFileService(IDiskProvider diskProvider, public ExistingExtraFileService(IEnumerable<IImportExistingExtraFiles> existingExtraFileImporters,
IDiskScanService diskScanService,
IEnumerable<IImportExistingExtraFiles> existingExtraFileImporters,
Logger logger) Logger logger)
{ {
_diskProvider = diskProvider;
_diskScanService = diskScanService;
_existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList(); _existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList();
_logger = logger; _logger = logger;
} }
public void Handle(SeriesScannedEvent message) public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles)
{ {
var series = message.Series;
if (!_diskProvider.FolderExists(series.Path))
{
return;
}
_logger.Debug("Looking for existing extra files in {0}", series.Path); _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<string>(); var importedFiles = new List<string>();
foreach (var existingExtraFileImporter in _existingExtraFileImporters) foreach (var existingExtraFileImporter in _existingExtraFileImporters)
@ -50,6 +38,15 @@ public void Handle(SeriesScannedEvent message)
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath))); importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
} }
return importedFiles;
}
public void Handle(SeriesScannedEvent message)
{
var series = message.Series;
var possibleExtraFiles = message.PossibleExtraFiles;
var importedFiles = ImportExtraFiles(series, possibleExtraFiles);
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count); _logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
} }
} }

View File

@ -17,6 +17,7 @@ namespace NzbDrone.Core.Extras
{ {
public interface IExtraService public interface IExtraService
{ {
void MoveFilesAfterRename(Series series, EpisodeFile episodeFile);
void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly);
} }
@ -139,6 +140,16 @@ public void Handle(EpisodeFolderCreatedEvent message)
} }
} }
public void MoveFilesAfterRename(Series series, EpisodeFile episodeFile)
{
var episodeFiles = new List<EpisodeFile> { episodeFile };
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.MoveFilesAfterRename(series, episodeFiles);
}
}
public void Handle(SeriesRenamedEvent message) public void Handle(SeriesRenamedEvent message)
{ {
var series = message.Series; var series = message.Series;

View File

@ -121,7 +121,7 @@ public void Scan(Series series)
} }
CleanMediaFiles(series, new List<string>()); CleanMediaFiles(series, new List<string>());
CompletedScanning(series); CompletedScanning(series, new List<string>());
return; return;
} }
@ -174,8 +174,11 @@ public void Scan(Series series)
fileInfoStopwatch.Stop(); fileInfoStopwatch.Stop();
_logger.Trace("Reprocessing existing files complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed); _logger.Trace("Reprocessing existing files complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);
var filesOnDisk = GetNonVideoFiles(series.Path);
var possibleExtraFiles = FilterPaths(series.Path, filesOnDisk);
RemoveEmptySeriesFolder(series.Path); RemoveEmptySeriesFolder(series.Path);
CompletedScanning(series); CompletedScanning(series, possibleExtraFiles);
} }
private void CleanMediaFiles(Series series, List<string> mediaFileList) private void CleanMediaFiles(Series series, List<string> mediaFileList)
@ -184,10 +187,10 @@ private void CleanMediaFiles(Series series, List<string> mediaFileList)
_mediaFileTableCleanupService.Clean(series, mediaFileList); _mediaFileTableCleanupService.Clean(series, mediaFileList);
} }
private void CompletedScanning(Series series) private void CompletedScanning(Series series, List<string> possibleExtraFiles)
{ {
_logger.Info("Completed scanning disk for {0}", series.Title); _logger.Info("Completed scanning disk for {0}", series.Title);
_eventAggregator.PublishEvent(new SeriesScannedEvent(series)); _eventAggregator.PublishEvent(new SeriesScannedEvent(series, possibleExtraFiles));
} }
public string[] GetVideoFiles(string path, bool allDirectories = true) public string[] GetVideoFiles(string path, bool allDirectories = true)

View File

@ -130,6 +130,7 @@ private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List<Ep
try try
{ {
MoveEpisodeFile(episodeFile, series, episodeFile.Episodes); MoveEpisodeFile(episodeFile, series, episodeFile.Episodes);
localEpisode.FileRenamedAfterScriptImport = true;
} }
catch (SameFilenameException) catch (SameFilenameException)
{ {

View File

@ -26,6 +26,7 @@ public class ImportApprovedEpisodes : IImportApprovedEpisodes
private readonly IUpgradeMediaFiles _episodeFileUpgrader; private readonly IUpgradeMediaFiles _episodeFileUpgrader;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IExtraService _extraService; private readonly IExtraService _extraService;
private readonly IExistingExtraFiles _existingExtraFiles;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
@ -34,6 +35,7 @@ public class ImportApprovedEpisodes : IImportApprovedEpisodes
public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader, public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IExtraService extraService, IExtraService extraService,
IExistingExtraFiles existingExtraFiles,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager, IManageCommandQueue commandQueueManager,
@ -42,6 +44,7 @@ public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader,
_episodeFileUpgrader = episodeFileUpgrader; _episodeFileUpgrader = episodeFileUpgrader;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_extraService = extraService; _extraService = extraService;
_existingExtraFiles = existingExtraFiles;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
@ -129,9 +132,22 @@ public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownloa
importResults.Add(new ImportResult(importDecision)); importResults.Add(new ImportResult(importDecision));
if (newDownload) if (newDownload)
{
if (localEpisode.ScriptImported)
{
_existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles);
if (localEpisode.FileRenamedAfterScriptImport)
{
_extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile);
}
}
if (!localEpisode.ScriptImported || localEpisode.ShouldImportExtras)
{ {
_extraService.ImportEpisode(localEpisode, episodeFile, copyOnly); _extraService.ImportEpisode(localEpisode, episodeFile, copyOnly);
} }
}
_eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, oldFiles, newDownload, downloadClientItem)); _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, oldFiles, newDownload, downloadClientItem));
} }

View File

@ -1,4 +1,5 @@
using NzbDrone.Common.Messaging; using System.Collections.Generic;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.Events namespace NzbDrone.Core.MediaFiles.Events
@ -6,10 +7,12 @@ namespace NzbDrone.Core.MediaFiles.Events
public class SeriesScannedEvent : IEvent public class SeriesScannedEvent : IEvent
{ {
public Series Series { get; private set; } public Series Series { get; private set; }
public List<string> PossibleExtraFiles { get; set; }
public SeriesScannedEvent(Series series) public SeriesScannedEvent(Series series, List<string> possibleExtraFiles)
{ {
Series = series; Series = series;
PossibleExtraFiles = possibleExtraFiles;
} }
} }
} }

View File

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -25,6 +27,7 @@ public class ImportScriptService : IImportScript
private readonly IProcessProvider _processProvider; private readonly IProcessProvider _processProvider;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ITagRepository _tagRepository; private readonly ITagRepository _tagRepository;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger; private readonly Logger _logger;
public ImportScriptService(IProcessProvider processProvider, public ImportScriptService(IProcessProvider processProvider,
@ -32,6 +35,7 @@ public ImportScriptService(IProcessProvider processProvider,
IConfigService configService, IConfigService configService,
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
ITagRepository tagRepository, ITagRepository tagRepository,
IDiskProvider diskProvider,
Logger logger) Logger logger)
{ {
_processProvider = processProvider; _processProvider = processProvider;
@ -39,9 +43,73 @@ public ImportScriptService(IProcessProvider processProvider,
_configService = configService; _configService = configService;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_tagRepository = tagRepository; _tagRepository = tagRepository;
_diskProvider = diskProvider;
_logger = logger; _logger = logger;
} }
private static readonly Regex OutputRegex = new Regex(@"^(?:\[(?:(?<mediaFile>MediaFile)|(?<extraFile>ExtraFile))\]\s?(?<fileName>.+)|(?<preventExtraImport>\[PreventExtraImport\])|\[MoveStatus\]\s?(?:(?<deferMove>DeferMove)|(?<moveComplete>MoveComplete)|(?<renameRequested>RenameRequested)))$", RegexOptions.Compiled);
private ScriptImportInfo ProcessOutput(List<ProcessOutputLine> processOutputLines)
{
var possibleExtraFiles = new List<string>();
string mediaFile = null;
var decision = ScriptImportDecision.MoveComplete;
var importExtraFiles = true;
foreach (var line in processOutputLines)
{
var match = OutputRegex.Match(line.Content);
if (match.Groups["mediaFile"].Success)
{
if (mediaFile is not null)
{
throw new ScriptImportException("Script output contains multiple media files. Only one media file can be returned.");
}
mediaFile = match.Groups["fileName"].Value;
if (!MediaFileExtensions.Extensions.Contains(Path.GetExtension(mediaFile)))
{
throw new ScriptImportException("Script output contains invalid media file: {0}", mediaFile);
}
else if (!_diskProvider.FileExists(mediaFile))
{
throw new ScriptImportException("Script output contains non-existent media file: {0}", mediaFile);
}
}
else if (match.Groups["extraFile"].Success)
{
var fileName = match.Groups["fileName"].Value;
if (!_diskProvider.FileExists(fileName))
{
_logger.Warn("Script output contains non-existent possible extra file: {0}", fileName);
}
possibleExtraFiles.Add(fileName);
}
else if (match.Groups["moveComplete"].Success)
{
decision = ScriptImportDecision.MoveComplete;
}
else if (match.Groups["renameRequested"].Success)
{
decision = ScriptImportDecision.RenameRequested;
}
else if (match.Groups["deferMove"].Success)
{
decision = ScriptImportDecision.DeferMove;
}
else if (match.Groups["preventExtraImport"].Success)
{
importExtraFiles = false;
}
}
return new ScriptImportInfo(possibleExtraFiles, mediaFile, decision, importExtraFiles);
}
public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalEpisode localEpisode, EpisodeFile episodeFile, TransferMode mode) public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalEpisode localEpisode, EpisodeFile episodeFile, TransferMode mode)
{ {
var series = localEpisode.Series; var series = localEpisode.Series;
@ -115,22 +183,37 @@ public ScriptImportDecision TryImport(string sourcePath, string destinationFileP
var processOutput = _processProvider.StartAndCapture(_configService.ScriptImportPath, $"\"{sourcePath}\" \"{destinationFilePath}\"", environmentVariables); var processOutput = _processProvider.StartAndCapture(_configService.ScriptImportPath, $"\"{sourcePath}\" \"{destinationFilePath}\"", environmentVariables);
_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)); _logger.Debug("Script Output: \r\n{0}", string.Join("\r\n", processOutput.Lines));
switch (processOutput.ExitCode) if (processOutput.ExitCode != 0)
{ {
case 0: // Copy complete throw new ScriptImportException("Script exited with non-zero exit code: {0}", processOutput.ExitCode);
return ScriptImportDecision.MoveComplete; }
case 2: // Copy complete, file potentially changed, should try renaming again
episodeFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(destinationFilePath); var scriptImportInfo = ProcessOutput(processOutput.Lines);
var mediaFile = scriptImportInfo.MediaFile ?? destinationFilePath;
localEpisode.PossibleExtraFiles = scriptImportInfo.PossibleExtraFiles;
episodeFile.RelativePath = series.Path.GetRelativePath(mediaFile);
episodeFile.Path = mediaFile;
var exitCode = processOutput.ExitCode;
localEpisode.ShouldImportExtras = scriptImportInfo.ImportExtraFiles;
if (scriptImportInfo.Decision != ScriptImportDecision.DeferMove)
{
localEpisode.ScriptImported = true;
}
if (scriptImportInfo.Decision == ScriptImportDecision.RenameRequested)
{
episodeFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(mediaFile);
episodeFile.Path = null; episodeFile.Path = null;
return ScriptImportDecision.RenameRequested; }
case 3: // Let Sonarr handle it
return ScriptImportDecision.DeferMove; return scriptImportInfo.Decision;
default: // Error, fail to import
throw new ScriptImportException("Moving with script failed! Exit code {0}", processOutput.ExitCode);
}
} }
} }
} }

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace NzbDrone.Core.MediaFiles
{
public struct ScriptImportInfo
{
public List<string> PossibleExtraFiles { get; set; }
public string MediaFile { get; set; }
public ScriptImportDecision Decision { get; set; }
public bool ImportExtraFiles { get; set; }
public ScriptImportInfo(List<string> possibleExtraFiles, string mediaFile, ScriptImportDecision decision, bool importExtraFiles)
{
PossibleExtraFiles = possibleExtraFiles;
MediaFile = mediaFile;
Decision = decision;
ImportExtraFiles = importExtraFiles;
}
}
}

View File

@ -40,6 +40,10 @@ public LocalEpisode()
public List<CustomFormat> CustomFormats { get; set; } public List<CustomFormat> CustomFormats { get; set; }
public int CustomFormatScore { get; set; } public int CustomFormatScore { get; set; }
public GrabbedReleaseInfo Release { get; set; } public GrabbedReleaseInfo Release { get; set; }
public bool ScriptImported { get; set; }
public bool FileRenamedAfterScriptImport { get; set; }
public bool ShouldImportExtras { get; set; }
public List<string> PossibleExtraFiles { get; set; }
public int SeasonNumber public int SeasonNumber
{ {