Skip to content

Commit d72f78d

Browse files
JeWe37mynameisbogdan
authored andcommittedJan 14, 2024
New: Custom import scripts can communicate information back
(cherry picked from commit b4ac495983d61819d9ab84f49c880957ba57418b)
1 parent dca9d69 commit d72f78d

File tree

11 files changed

+187
-48
lines changed

11 files changed

+187
-48
lines changed
 

‎src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public void should_not_scan_extras_subfolder()
156156
Subject.Scan(_movie);
157157

158158
Mocker.GetMock<IDiskProvider>()
159-
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<bool>()), Times.Once());
159+
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<bool>()), Times.Exactly(2));
160160

161161
Mocker.GetMock<IMakeImportDecision>()
162162
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());

‎src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
23
using FizzWare.NBuilder;
34
using FluentAssertions;
45
using Moq;
@@ -71,7 +72,7 @@ public void should_skip_up_to_date_media_info()
7172
GivenFileExists();
7273
GivenSuccessfulScan();
7374

74-
Subject.Handle(new MovieScannedEvent(_movie));
75+
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
7576

7677
Mocker.GetMock<IVideoFileInfoReader>()
7778
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2));
@@ -97,7 +98,7 @@ public void should_skip_not_yet_date_media_info()
9798
GivenFileExists();
9899
GivenSuccessfulScan();
99100

100-
Subject.Handle(new MovieScannedEvent(_movie));
101+
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
101102

102103
Mocker.GetMock<IVideoFileInfoReader>()
103104
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2));
@@ -123,7 +124,7 @@ public void should_update_outdated_media_info()
123124
GivenFileExists();
124125
GivenSuccessfulScan();
125126

126-
Subject.Handle(new MovieScannedEvent(_movie));
127+
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
127128

128129
Mocker.GetMock<IVideoFileInfoReader>()
129130
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(3));
@@ -146,7 +147,7 @@ public void should_ignore_missing_files()
146147

147148
GivenSuccessfulScan();
148149

149-
Subject.Handle(new MovieScannedEvent(_movie));
150+
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
150151

151152
Mocker.GetMock<IVideoFileInfoReader>()
152153
.Verify(v => v.GetMediaInfo("media.mkv"), Times.Never());
@@ -173,7 +174,7 @@ public void should_continue_after_failure()
173174
GivenSuccessfulScan();
174175
GivenFailedScan(Path.Combine(_movie.Path, "media2.mkv"));
175176

176-
Subject.Handle(new MovieScannedEvent(_movie));
177+
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
177178

178179
Mocker.GetMock<IVideoFileInfoReader>()
179180
.Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(1));
@@ -203,7 +204,7 @@ public void should_not_update_files_if_media_info_disabled()
203204
GivenFileExists();
204205
GivenSuccessfulScan();
205206

206-
Subject.Handle(new MovieScannedEvent(_movie));
207+
Subject.Handle(new MovieScannedEvent(_movie, new List<string>()));
207208

208209
Mocker.GetMock<IVideoFileInfoReader>()
209210
.Verify(v => v.GetMediaInfo(It.IsAny<string>()), Times.Never());

‎src/NzbDrone.Core/Extras/ExistingExtraFileService.cs

+18-21
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,33 @@
22
using System.IO;
33
using System.Linq;
44
using NLog;
5-
using NzbDrone.Common.Disk;
6-
using NzbDrone.Core.MediaFiles;
75
using NzbDrone.Core.MediaFiles.Events;
86
using NzbDrone.Core.Messaging.Events;
7+
using NzbDrone.Core.Movies;
98

109
namespace NzbDrone.Core.Extras
1110
{
12-
public class ExistingExtraFileService : IHandle<MovieScannedEvent>
11+
public interface IExistingExtraFiles
12+
{
13+
List<string> ImportExtraFiles(Movie movie, List<string> possibleExtraFiles);
14+
}
15+
16+
public class ExistingExtraFileService : IExistingExtraFiles, IHandle<MovieScannedEvent>
1317
{
14-
private readonly IDiskProvider _diskProvider;
15-
private readonly IDiskScanService _diskScanService;
1618
private readonly List<IImportExistingExtraFiles> _existingExtraFileImporters;
1719
private readonly Logger _logger;
1820

19-
public ExistingExtraFileService(IDiskProvider diskProvider,
20-
IDiskScanService diskScanService,
21-
IEnumerable<IImportExistingExtraFiles> existingExtraFileImporters,
21+
public ExistingExtraFileService(IEnumerable<IImportExistingExtraFiles> existingExtraFileImporters,
2222
Logger logger)
2323
{
24-
_diskProvider = diskProvider;
25-
_diskScanService = diskScanService;
2624
_existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList();
2725
_logger = logger;
2826
}
2927

30-
public void Handle(MovieScannedEvent message)
28+
public List<string> ImportExtraFiles(Movie movie, List<string> possibleExtraFiles)
3129
{
32-
var movie = message.Movie;
33-
34-
if (!_diskProvider.FolderExists(movie.Path))
35-
{
36-
return;
37-
}
38-
3930
_logger.Debug("Looking for existing extra files in {0}", movie.Path);
4031

41-
var filesOnDisk = _diskScanService.GetNonVideoFiles(movie.Path);
42-
var possibleExtraFiles = _diskScanService.FilterPaths(movie.Path, filesOnDisk, false);
43-
4432
var importedFiles = new List<string>();
4533

4634
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
@@ -50,6 +38,15 @@ public void Handle(MovieScannedEvent message)
5038
importedFiles.AddRange(imported.Select(f => Path.Combine(movie.Path, f.RelativePath)));
5139
}
5240

41+
return importedFiles;
42+
}
43+
44+
public void Handle(MovieScannedEvent message)
45+
{
46+
var movie = message.Movie;
47+
var possibleExtraFiles = message.PossibleExtraFiles;
48+
var importedFiles = ImportExtraFiles(movie, possibleExtraFiles);
49+
5350
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
5451
}
5552
}

‎src/NzbDrone.Core/Extras/ExtraService.cs

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Extras
1717
{
1818
public interface IExtraService
1919
{
20+
void MoveFilesAfterRename(Movie movie, MovieFile movieFile);
2021
void ImportMovie(LocalMovie localMovie, MovieFile movieFile, bool isReadOnly);
2122
}
2223

@@ -139,6 +140,16 @@ public void Handle(MovieFolderCreatedEvent message)
139140
}
140141
}
141142

143+
public void MoveFilesAfterRename(Movie movie, MovieFile movieFile)
144+
{
145+
var movieFiles = new List<MovieFile> { movieFile };
146+
147+
foreach (var extraFileManager in _extraFileManagers)
148+
{
149+
extraFileManager.MoveFilesAfterRename(movie, movieFiles);
150+
}
151+
}
152+
142153
public void Handle(MovieRenamedEvent message)
143154
{
144155
var movie = message.Movie;

‎src/NzbDrone.Core/MediaFiles/DiskScanService.cs

+7-4
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public void Scan(Movie movie)
121121
}
122122

123123
CleanMediaFiles(movie, new List<string>());
124-
CompletedScanning(movie);
124+
CompletedScanning(movie, new List<string>());
125125

126126
return;
127127
}
@@ -173,8 +173,11 @@ public void Scan(Movie movie)
173173
fileInfoStopwatch.Stop();
174174
_logger.Trace("Reprocessing existing files complete for: {0} [{1}]", movie, decisionsStopwatch.Elapsed);
175175

176+
var filesOnDisk = GetNonVideoFiles(movie.Path);
177+
var possibleExtraFiles = FilterPaths(movie.Path, filesOnDisk);
178+
176179
RemoveEmptyMovieFolder(movie.Path);
177-
CompletedScanning(movie);
180+
CompletedScanning(movie, possibleExtraFiles);
178181
}
179182

180183
private void CleanMediaFiles(Movie movie, List<string> mediaFileList)
@@ -183,10 +186,10 @@ private void CleanMediaFiles(Movie movie, List<string> mediaFileList)
183186
_mediaFileTableCleanupService.Clean(movie, mediaFileList);
184187
}
185188

186-
private void CompletedScanning(Movie movie)
189+
private void CompletedScanning(Movie movie, List<string> possibleExtraFiles)
187190
{
188191
_logger.Info("Completed scanning disk for {0}", movie.Title);
189-
_eventAggregator.PublishEvent(new MovieScannedEvent(movie));
192+
_eventAggregator.PublishEvent(new MovieScannedEvent(movie, possibleExtraFiles));
190193
}
191194

192195
public string[] GetVideoFiles(string path, bool allDirectories = true)
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
using NzbDrone.Common.Messaging;
1+
using System.Collections.Generic;
2+
using NzbDrone.Common.Messaging;
23
using NzbDrone.Core.Movies;
34

45
namespace NzbDrone.Core.MediaFiles.Events
56
{
67
public class MovieScannedEvent : IEvent
78
{
89
public Movie Movie { get; private set; }
10+
public List<string> PossibleExtraFiles { get; set; }
911

10-
public MovieScannedEvent(Movie movie)
12+
public MovieScannedEvent(Movie movie, List<string> possibleExtraFiles)
1113
{
1214
Movie = movie;
15+
PossibleExtraFiles = possibleExtraFiles;
1316
}
1417
}
1518
}

‎src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ private MovieFile TransferFile(MovieFile movieFile, Movie movie, string destinat
126126
try
127127
{
128128
MoveMovieFile(movieFile, movie);
129+
localMovie.FileRenamedAfterScriptImport = true;
129130
}
130131
catch (SameFilenameException)
131132
{

‎src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class ImportApprovedMovie : IImportApprovedMovie
2727
private readonly IUpgradeMediaFiles _movieFileUpgrader;
2828
private readonly IMediaFileService _mediaFileService;
2929
private readonly IExtraService _extraService;
30+
private readonly IExistingExtraFiles _existingExtraFiles;
3031
private readonly IDiskProvider _diskProvider;
3132
private readonly IHistoryService _historyService;
3233
private readonly IEventAggregator _eventAggregator;
@@ -36,6 +37,7 @@ public class ImportApprovedMovie : IImportApprovedMovie
3637
public ImportApprovedMovie(IUpgradeMediaFiles movieFileUpgrader,
3738
IMediaFileService mediaFileService,
3839
IExtraService extraService,
40+
IExistingExtraFiles existingExtraFiles,
3941
IDiskProvider diskProvider,
4042
IHistoryService historyService,
4143
IEventAggregator eventAggregator,
@@ -45,6 +47,7 @@ public ImportApprovedMovie(IUpgradeMediaFiles movieFileUpgrader,
4547
_movieFileUpgrader = movieFileUpgrader;
4648
_mediaFileService = mediaFileService;
4749
_extraService = extraService;
50+
_existingExtraFiles = existingExtraFiles;
4851
_diskProvider = diskProvider;
4952
_historyService = historyService;
5053
_eventAggregator = eventAggregator;
@@ -146,7 +149,20 @@ public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownloa
146149

147150
if (newDownload)
148151
{
149-
_extraService.ImportMovie(localMovie, movieFile, copyOnly);
152+
if (localMovie.ScriptImported)
153+
{
154+
_existingExtraFiles.ImportExtraFiles(localMovie.Movie, localMovie.PossibleExtraFiles);
155+
156+
if (localMovie.FileRenamedAfterScriptImport)
157+
{
158+
_extraService.MoveFilesAfterRename(localMovie.Movie, movieFile);
159+
}
160+
}
161+
162+
if (!localMovie.ScriptImported || localMovie.ShouldImportExtras)
163+
{
164+
_extraService.ImportMovie(localMovie, movieFile, copyOnly);
165+
}
150166
}
151167

152168
_eventAggregator.PublishEvent(new MovieFileImportedEvent(localMovie, movieFile, oldFiles, newDownload, downloadClientItem));

‎src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs

+95-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System.Collections.Generic;
12
using System.Collections.Specialized;
23
using System.IO;
34
using System.Linq;
5+
using System.Text.RegularExpressions;
46
using NLog;
57
using NzbDrone.Common.Disk;
68
using NzbDrone.Common.Extensions;
@@ -25,23 +27,89 @@ public class ImportScriptService : IImportScript
2527
private readonly IProcessProvider _processProvider;
2628
private readonly IConfigService _configService;
2729
private readonly ITagRepository _tagRepository;
30+
private readonly IDiskProvider _diskProvider;
2831
private readonly Logger _logger;
2932

3033
public ImportScriptService(IProcessProvider processProvider,
3134
IVideoFileInfoReader videoFileInfoReader,
3235
IConfigService configService,
3336
IConfigFileProvider configFileProvider,
3437
ITagRepository tagRepository,
38+
IDiskProvider diskProvider,
3539
Logger logger)
3640
{
3741
_processProvider = processProvider;
3842
_videoFileInfoReader = videoFileInfoReader;
3943
_configService = configService;
4044
_configFileProvider = configFileProvider;
4145
_tagRepository = tagRepository;
46+
_diskProvider = diskProvider;
4247
_logger = logger;
4348
}
4449

50+
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);
51+
52+
private ScriptImportInfo ProcessOutput(List<ProcessOutputLine> processOutputLines)
53+
{
54+
var possibleExtraFiles = new List<string>();
55+
string mediaFile = null;
56+
var decision = ScriptImportDecision.MoveComplete;
57+
var importExtraFiles = true;
58+
59+
foreach (var line in processOutputLines)
60+
{
61+
var match = OutputRegex.Match(line.Content);
62+
63+
if (match.Groups["mediaFile"].Success)
64+
{
65+
if (mediaFile is not null)
66+
{
67+
throw new ScriptImportException("Script output contains multiple media files. Only one media file can be returned.");
68+
}
69+
70+
mediaFile = match.Groups["fileName"].Value;
71+
72+
if (!MediaFileExtensions.Extensions.Contains(Path.GetExtension(mediaFile)))
73+
{
74+
throw new ScriptImportException("Script output contains invalid media file: {0}", mediaFile);
75+
}
76+
else if (!_diskProvider.FileExists(mediaFile))
77+
{
78+
throw new ScriptImportException("Script output contains non-existent media file: {0}", mediaFile);
79+
}
80+
}
81+
else if (match.Groups["extraFile"].Success)
82+
{
83+
var fileName = match.Groups["fileName"].Value;
84+
85+
if (!_diskProvider.FileExists(fileName))
86+
{
87+
_logger.Warn("Script output contains non-existent possible extra file: {0}", fileName);
88+
}
89+
90+
possibleExtraFiles.Add(fileName);
91+
}
92+
else if (match.Groups["moveComplete"].Success)
93+
{
94+
decision = ScriptImportDecision.MoveComplete;
95+
}
96+
else if (match.Groups["renameRequested"].Success)
97+
{
98+
decision = ScriptImportDecision.RenameRequested;
99+
}
100+
else if (match.Groups["deferMove"].Success)
101+
{
102+
decision = ScriptImportDecision.DeferMove;
103+
}
104+
else if (match.Groups["preventExtraImport"].Success)
105+
{
106+
importExtraFiles = false;
107+
}
108+
}
109+
110+
return new ScriptImportInfo(possibleExtraFiles, mediaFile, decision, importExtraFiles);
111+
}
112+
45113
public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalMovie localMovie, MovieFile movieFile, TransferMode mode)
46114
{
47115
var movie = localMovie.Movie;
@@ -111,22 +179,37 @@ public ScriptImportDecision TryImport(string sourcePath, string destinationFileP
111179

112180
var processOutput = _processProvider.StartAndCapture(_configService.ScriptImportPath, $"\"{sourcePath}\" \"{destinationFilePath}\"", environmentVariables);
113181

114-
_logger.Debug("Executed external script: {0} - Status: {1}", _configService.ScriptImportPath, processOutput.ExitCode);
115182
_logger.Debug("Script Output: \r\n{0}", string.Join("\r\n", processOutput.Lines));
116183

117-
switch (processOutput.ExitCode)
184+
if (processOutput.ExitCode != 0)
185+
{
186+
throw new ScriptImportException("Script exited with non-zero exit code: {0}", processOutput.ExitCode);
187+
}
188+
189+
var scriptImportInfo = ProcessOutput(processOutput.Lines);
190+
191+
var mediaFile = scriptImportInfo.MediaFile ?? destinationFilePath;
192+
localMovie.PossibleExtraFiles = scriptImportInfo.PossibleExtraFiles;
193+
194+
movieFile.RelativePath = movie.Path.GetRelativePath(mediaFile);
195+
movieFile.Path = mediaFile;
196+
197+
var exitCode = processOutput.ExitCode;
198+
199+
localMovie.ShouldImportExtras = scriptImportInfo.ImportExtraFiles;
200+
201+
if (scriptImportInfo.Decision != ScriptImportDecision.DeferMove)
118202
{
119-
case 0: // Copy complete
120-
return ScriptImportDecision.MoveComplete;
121-
case 2: // Copy complete, file potentially changed, should try renaming again
122-
movieFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(destinationFilePath);
123-
movieFile.Path = null;
124-
return ScriptImportDecision.RenameRequested;
125-
case 3: // Let Radarr handle it
126-
return ScriptImportDecision.DeferMove;
127-
default: // Error, fail to import
128-
throw new ScriptImportException("Moving with script failed! Exit code {0}", processOutput.ExitCode);
203+
localMovie.ScriptImported = true;
129204
}
205+
206+
if (scriptImportInfo.Decision == ScriptImportDecision.RenameRequested)
207+
{
208+
movieFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(mediaFile);
209+
movieFile.Path = null;
210+
}
211+
212+
return scriptImportInfo.Decision;
130213
}
131214
}
132215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Collections.Generic;
2+
3+
namespace NzbDrone.Core.MediaFiles
4+
{
5+
public struct ScriptImportInfo
6+
{
7+
public List<string> PossibleExtraFiles { get; set; }
8+
public string MediaFile { get; set; }
9+
public ScriptImportDecision Decision { get; set; }
10+
public bool ImportExtraFiles { get; set; }
11+
12+
public ScriptImportInfo(List<string> possibleExtraFiles, string mediaFile, ScriptImportDecision decision, bool importExtraFiles)
13+
{
14+
PossibleExtraFiles = possibleExtraFiles;
15+
MediaFile = mediaFile;
16+
Decision = decision;
17+
ImportExtraFiles = importExtraFiles;
18+
}
19+
}
20+
}

‎src/NzbDrone.Core/Parser/Model/LocalMovie.cs

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public LocalMovie()
3636
public List<CustomFormat> CustomFormats { get; set; }
3737
public int CustomFormatScore { get; set; }
3838
public GrabbedReleaseInfo Release { get; set; }
39+
public bool ScriptImported { get; set; }
40+
public bool FileRenamedAfterScriptImport { get; set; }
41+
public bool ShouldImportExtras { get; set; }
42+
public List<string> PossibleExtraFiles { get; set; }
3943

4044
public override string ToString()
4145
{

0 commit comments

Comments
 (0)
Please sign in to comment.