diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js
index 627263fff..5a12e6e72 100644
--- a/frontend/src/Settings/MediaManagement/MediaManagement.js
+++ b/frontend/src/Settings/MediaManagement/MediaManagement.js
@@ -124,29 +124,29 @@ class MediaManagement extends Component {
{
- isFetching &&
+ isFetching ?
+ : null
}
{
- !isFetching && error &&
+ !isFetching && error ?
+ : null
}
{
- hasSettings && !isFetching && !error &&
+ hasSettings && !isFetching && !error ?
+ : null
}
diff --git a/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs b/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs
index 8a19c6175..c1be6cbef 100644
--- a/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs
+++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs
@@ -32,6 +32,7 @@ public MediaManagementConfigController(IConfigService configService,
.When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx));
+ SharedValidator.RuleFor(c => c.ScriptImportPath).IsValidPath().When(c => c.UseScriptImport);
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
}
diff --git a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs
index e318c3c82..7f6e3030c 100644
--- a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs
+++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs
@@ -25,6 +25,9 @@ public class MediaManagementConfigResource : RestResource
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public int MinimumFreeSpaceWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; }
+ public bool EnableMediaInfo { get; set; }
+ public bool UseScriptImport { get; set; }
+ public string ScriptImportPath { get; set; }
public bool ImportExtraFiles { get; set; }
public string ExtraFileExtensions { get; set; }
}
@@ -53,6 +56,9 @@ public static MediaManagementConfigResource ToResource(IConfigService model)
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting,
CopyUsingHardlinks = model.CopyUsingHardlinks,
+ EnableMediaInfo = model.EnableMediaInfo,
+ UseScriptImport = model.UseScriptImport,
+ ScriptImportPath = model.ScriptImportPath,
ImportExtraFiles = model.ImportExtraFiles,
ExtraFileExtensions = model.ExtraFileExtensions,
};
diff --git a/src/Lidarr.Api.V1/openapi.json b/src/Lidarr.Api.V1/openapi.json
index 4c0462717..3d3f3abec 100644
--- a/src/Lidarr.Api.V1/openapi.json
+++ b/src/Lidarr.Api.V1/openapi.json
@@ -10870,6 +10870,12 @@
"copyUsingHardlinks": {
"type": "boolean"
},
+ "useScriptImport": {
+ "type": "boolean"
+ },
+ "scriptImportPath": {
+ "type": "string"
+ },
"importExtraFiles": {
"type": "boolean"
},
diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs
index 34b94c927..6983f1b54 100644
--- a/src/NzbDrone.Core/Configuration/ConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/ConfigService.cs
@@ -207,6 +207,27 @@ public bool CopyUsingHardlinks
set { SetValue("CopyUsingHardlinks", value); }
}
+ public bool EnableMediaInfo
+ {
+ get { return GetValueBoolean("EnableMediaInfo", true); }
+
+ set { SetValue("EnableMediaInfo", value); }
+ }
+
+ public bool UseScriptImport
+ {
+ get { return GetValueBoolean("UseScriptImport", false); }
+
+ set { SetValue("UseScriptImport", value); }
+ }
+
+ public string ScriptImportPath
+ {
+ get { return GetValue("ScriptImportPath"); }
+
+ set { SetValue("ScriptImportPath", value); }
+ }
+
public bool ImportExtraFiles
{
get { return GetValueBoolean("ImportExtraFiles", false); }
diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs
index 1665334d4..c78f060ae 100644
--- a/src/NzbDrone.Core/Configuration/IConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/IConfigService.cs
@@ -33,6 +33,9 @@ public interface IConfigService
bool SkipFreeSpaceCheckWhenImporting { get; set; }
int MinimumFreeSpaceWhenImporting { get; set; }
bool CopyUsingHardlinks { get; set; }
+ bool EnableMediaInfo { get; set; }
+ bool UseScriptImport { get; set; }
+ string ScriptImportPath { get; set; }
bool ImportExtraFiles { get; set; }
string ExtraFileExtensions { get; set; }
bool WatchLibraryForChanges { get; set; }
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index ade1d9d2b..2b1605702 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -594,6 +594,10 @@
"ImportLists": "Import Lists",
"ImportListsSettingsSummary": "Import from another {appName} instance or Trakt lists and manage list exclusions",
"ImportMechanismHealthCheckMessage": "Enable Completed Download Handling",
+ "ImportScriptPath": "Import Script Path",
+ "ImportScriptPathHelpText": "The path to the script to use for importing",
+ "ImportUsingScript": "Import Using Script",
+ "ImportUsingScriptHelpText": "Copy files for importing using a script (ex. for transcoding)",
"ImportedTo": "Imported To",
"Importing": "Importing",
"Inactive": "Inactive",
diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs
new file mode 100644
index 000000000..fc0404ca0
--- /dev/null
+++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs
@@ -0,0 +1,125 @@
+using System.Collections.Specialized;
+using System.Linq;
+using NLog;
+using NzbDrone.Common.Disk;
+using NzbDrone.Common.Processes;
+using NzbDrone.Core.Configuration;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.Download;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Tags;
+
+namespace NzbDrone.Core.MediaFiles
+{
+ public interface IImportScript
+ {
+ public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalTrack localTrack, TrackFile trackFile, TransferMode mode, DownloadClientItem downloadClientItem = null);
+ }
+
+ public class ImportScriptService : IImportScript
+ {
+ private readonly IConfigFileProvider _configFileProvider;
+ private readonly IAudioTagService _audioTagService;
+ private readonly IProcessProvider _processProvider;
+ private readonly IConfigService _configService;
+ private readonly ITagRepository _tagRepository;
+ private readonly ICustomFormatCalculationService _customFormatCalculationService;
+ private readonly Logger _logger;
+
+ public ImportScriptService(IProcessProvider processProvider,
+ IAudioTagService audioTagService,
+ IConfigService configService,
+ IConfigFileProvider configFileProvider,
+ ITagRepository tagRepository,
+ ICustomFormatCalculationService customFormatCalculationService,
+ Logger logger)
+ {
+ _processProvider = processProvider;
+ _audioTagService = audioTagService;
+ _configService = configService;
+ _configFileProvider = configFileProvider;
+ _tagRepository = tagRepository;
+ _customFormatCalculationService = customFormatCalculationService;
+ _logger = logger;
+ }
+
+ public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalTrack localTrack, TrackFile trackFile, TransferMode mode, DownloadClientItem downloadClientItem = null)
+ {
+ var artist = localTrack.Artist;
+ var album = localTrack.Album;
+ var downloadClientInfo = downloadClientItem?.DownloadClientInfo;
+ var downloadId = downloadClientItem?.DownloadId;
+
+ if (!_configService.UseScriptImport)
+ {
+ return ScriptImportDecision.DeferMove;
+ }
+
+ var environmentVariables = new StringDictionary
+ {
+ { "Lidarr_SourcePath", sourcePath },
+ { "Lidarr_DestinationPath", destinationFilePath },
+ { "Lidarr_InstanceName", _configFileProvider.InstanceName },
+ { "Lidarr_ApplicationUrl", _configService.ApplicationUrl },
+ { "Lidarr_TransferMode", mode.ToString() },
+ { "Lidarr_Artist_Id", artist.Id.ToString() },
+ { "Lidarr_Artist_Name", artist.Name },
+ { "Lidarr_Artist_Path", artist.Path },
+ { "Lidarr_Artist_MBId", artist.ForeignArtistId },
+ { "Lidarr_Artist_Tags", string.Join("|", artist.Tags.Select(t => _tagRepository.Get(t).Label)) },
+ { "Lidarr_Album_Id", album.Id.ToString() },
+ { "Lidarr_Album_Title", album.Title },
+ { "Lidarr_Album_MBId", album.ForeignAlbumId },
+ { "Lidarr_Album_ReleaseDate", album.ReleaseDate?.ToString("yyyy-MM-dd") ?? string.Empty },
+ { "Lidarr_Album_Genres", string.Join("|", album.Genres) },
+ { "Lidarr_TrackFile_TrackCount", localTrack.Tracks.Count.ToString() },
+ { "Lidarr_TrackFile_TrackIds", string.Join(",", localTrack.Tracks.Select(t => t.Id)) },
+ { "Lidarr_TrackFile_TrackNumbers", string.Join(",", localTrack.Tracks.Select(t => t.TrackNumber)) },
+ { "Lidarr_TrackFile_TrackTitles", string.Join("|", localTrack.Tracks.Select(t => t.Title)) },
+ { "Lidarr_TrackFile_Quality", localTrack.Quality.Quality.Name },
+ { "Lidarr_TrackFile_QualityVersion", localTrack.Quality.Revision.Version.ToString() },
+ { "Lidarr_TrackFile_ReleaseGroup", localTrack.ReleaseGroup ?? string.Empty },
+ { "Lidarr_TrackFile_SceneName", localTrack.SceneName ?? string.Empty },
+ { "Lidarr_Download_Client", downloadClientInfo?.Name ?? string.Empty },
+ { "Lidarr_Download_Client_Type", downloadClientInfo?.Type ?? string.Empty },
+ { "Lidarr_Download_Id", downloadId ?? string.Empty }
+ };
+
+ // Audio-specific MediaInfo (no video properties for music files)
+ if (localTrack.FileTrackInfo?.MediaInfo != null)
+ {
+ var mediaInfo = localTrack.FileTrackInfo.MediaInfo;
+ environmentVariables.Add("Lidarr_TrackFile_MediaInfo_AudioChannels", mediaInfo.AudioChannels.ToString());
+ environmentVariables.Add("Lidarr_TrackFile_MediaInfo_AudioCodec", mediaInfo.AudioFormat ?? string.Empty);
+ environmentVariables.Add("Lidarr_TrackFile_MediaInfo_AudioBitRate", mediaInfo.AudioBitrate.ToString());
+ environmentVariables.Add("Lidarr_TrackFile_MediaInfo_AudioSampleRate", mediaInfo.AudioSampleRate.ToString());
+ environmentVariables.Add("Lidarr_TrackFile_MediaInfo_BitsPerSample", mediaInfo.AudioBits.ToString());
+ }
+
+ // CustomFormats for music files
+ var customFormats = _customFormatCalculationService.ParseCustomFormat(localTrack);
+ environmentVariables.Add("Lidarr_TrackFile_CustomFormat", string.Join("|", customFormats.Select(x => x.Name)));
+
+ _logger.Debug("Executing external script: {0}", _configService.ScriptImportPath);
+
+ 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));
+
+ switch (processOutput.ExitCode)
+ {
+ case 0: // Copy complete
+ return ScriptImportDecision.MoveComplete;
+ case 2: // Copy complete, file potentially changed, should try renaming again
+ trackFile.MediaInfo = _audioTagService.ReadTags(destinationFilePath).MediaInfo;
+ trackFile.Path = null;
+ return ScriptImportDecision.RenameRequested;
+ case 3: // Let Lidarr handle it
+ return ScriptImportDecision.DeferMove;
+ default: // Error, fail to import
+ throw new ScriptImportException("Moving with script failed! Exit code {0}", processOutput.ExitCode);
+ }
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecision.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecision.cs
new file mode 100644
index 000000000..fb2eb4f6f
--- /dev/null
+++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecision.cs
@@ -0,0 +1,10 @@
+namespace NzbDrone.Core.MediaFiles
+{
+ public enum ScriptImportDecision
+ {
+ MoveComplete,
+ RenameRequested,
+ RejectExtra,
+ DeferMove
+ }
+}
diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportException.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportException.cs
new file mode 100644
index 000000000..9ac0f49d4
--- /dev/null
+++ b/src/NzbDrone.Core/MediaFiles/ScriptImportException.cs
@@ -0,0 +1,23 @@
+using System;
+using NzbDrone.Common.Exceptions;
+
+namespace NzbDrone.Core.MediaFiles
+{
+ public class ScriptImportException : NzbDroneException
+ {
+ public ScriptImportException(string message)
+ : base(message)
+ {
+ }
+
+ public ScriptImportException(string message, params object[] args)
+ : base(message, args)
+ {
+ }
+
+ public ScriptImportException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs
index 68bc6c21d..151fee3ff 100644
--- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs
+++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs
@@ -35,6 +35,7 @@ public class TrackFileMovingService : IMoveTrackFiles
private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IRootFolderService _rootFolderService;
private readonly IEventAggregator _eventAggregator;
+ private readonly IImportScript _scriptImportDecider;
private readonly IConfigService _configService;
private readonly Logger _logger;
@@ -48,6 +49,7 @@ public TrackFileMovingService(ITrackService trackService,
IMediaFileAttributeService mediaFileAttributeService,
IRootFolderService rootFolderService,
IEventAggregator eventAggregator,
+ IImportScript scriptImportDecider,
IConfigService configService,
Logger logger)
{
@@ -61,6 +63,7 @@ public TrackFileMovingService(ITrackService trackService,
_mediaFileAttributeService = mediaFileAttributeService;
_rootFolderService = rootFolderService;
_eventAggregator = eventAggregator;
+ _scriptImportDecider = scriptImportDecider;
_configService = configService;
_logger = logger;
}
@@ -86,7 +89,7 @@ public TrackFile MoveTrackFile(TrackFile trackFile, LocalTrack localTrack)
_logger.Debug("Moving track file: {0} to {1}", trackFile.Path, filePath);
- return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Move);
+ return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Move, localTrack);
}
public TrackFile CopyTrackFile(TrackFile trackFile, LocalTrack localTrack)
@@ -98,14 +101,14 @@ public TrackFile CopyTrackFile(TrackFile trackFile, LocalTrack localTrack)
if (_configService.CopyUsingHardlinks)
{
_logger.Debug("Attempting to hardlink track file: {0} to {1}", trackFile.Path, filePath);
- return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.HardLinkOrCopy);
+ return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.HardLinkOrCopy, localTrack);
}
_logger.Debug("Copying track file: {0} to {1}", trackFile.Path, filePath);
- return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy);
+ return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy, localTrack);
}
- private TrackFile TransferFile(TrackFile trackFile, Artist artist, List