diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index c9ec49e62..7bac20529 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -68,29 +68,29 @@ class MediaManagement extends Component { { - isFetching && + isFetching ?
-
+ : null } { - !isFetching && error && + !isFetching && error ?
{translate('UnableToLoadMediaManagementSettings')}
-
+ : null } { - hasSettings && !isFetching && !error && + hasSettings && !isFetching && !error ?
{ - advancedSettings && + advancedSettings ?
-
+ : null } { - advancedSettings && + advancedSettings ?
@@ -194,6 +194,41 @@ class MediaManagement extends Component { /> + + {translate('ImportUsingScript')} + + + + + { + settings.useScriptImport.value ? + + {translate('ImportScriptPath')} + + + : null + } + {translate('ImportExtraFiles')} @@ -209,7 +244,7 @@ class MediaManagement extends Component { { - settings.importExtraFiles.value && + settings.importExtraFiles.value ? - + : null } -
+ : null }
{ - advancedSettings && !isWindows && + advancedSettings && !isWindows ?
@@ -434,9 +469,9 @@ class MediaManagement extends Component { {...settings.chownGroup} /> -
+
: null } -
+ : null } diff --git a/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs b/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs index 8a19c6175..729f013b9 100644 --- a/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs +++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigController.cs @@ -32,6 +32,9 @@ namespace Lidarr.Api.V1.Config .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..7779ef035 100644 --- a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs +++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs @@ -25,6 +25,8 @@ namespace Lidarr.Api.V1.Config public bool SkipFreeSpaceCheckWhenImporting { get; set; } public int MinimumFreeSpaceWhenImporting { get; set; } public bool CopyUsingHardlinks { get; set; } + public bool UseScriptImport { get; set; } + public string ScriptImportPath { get; set; } public bool ImportExtraFiles { get; set; } public string ExtraFileExtensions { get; set; } } @@ -53,6 +55,8 @@ namespace Lidarr.Api.V1.Config SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting, MinimumFreeSpaceWhenImporting = model.MinimumFreeSpaceWhenImporting, CopyUsingHardlinks = model.CopyUsingHardlinks, + UseScriptImport = model.UseScriptImport, + ScriptImportPath = model.ScriptImportPath, ImportExtraFiles = model.ImportExtraFiles, ExtraFileExtensions = model.ExtraFileExtensions, }; diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 8e74fcdf9..f7e195e81 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -200,6 +200,20 @@ namespace NzbDrone.Core.Configuration set { SetValue("CopyUsingHardlinks", 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 6cef2ad88..674249ab7 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -32,6 +32,8 @@ namespace NzbDrone.Core.Configuration bool SkipFreeSpaceCheckWhenImporting { get; set; } int MinimumFreeSpaceWhenImporting { get; set; } bool CopyUsingHardlinks { 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 4ea0c4962..f5c602dce 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -392,6 +392,8 @@ "ImportListStatusCheckSingleClientMessage": "Lists unavailable due to failures: {0}", "ImportLists": "Import Lists", "ImportMechanismHealthCheckMessage": "Enable Completed Download Handling", + "ImportScriptPath": "Import Script Path", + "ImportUsingScript": "Import Using Script", "ImportedTo": "Imported To", "Importing": "Importing", "Inactive": "Inactive", @@ -754,6 +756,7 @@ "SceneInformation": "Scene Information", "SceneNumberHasntBeenVerifiedYet": "Scene number hasn't been verified yet", "Scheduled": "Scheduled", + "ScriptImportPathHelpText": "The path to the script to use for importing", "ScriptPath": "Script Path", "ScrubAudioTagsHelpText": "Remove existing tags from files, leaving only those added by Lidarr.", "ScrubExistingTags": "Scrub Existing Tags", @@ -963,6 +966,7 @@ "UrlBaseHelpTextWarning": "Requires restart to take effect", "UseHardlinksInsteadOfCopy": "Use Hardlinks instead of Copy", "UseProxy": "Use Proxy", + "UseScriptImportHelpText": "Copy files for importing using a script (ex. for transcoding)", "Usenet": "Usenet", "UsenetDelay": "Usenet Delay", "UsenetDelayHelpText": "Delay in minutes to wait before grabbing a release from Usenet", diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs new file mode 100644 index 000000000..043d0ec3b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs @@ -0,0 +1,100 @@ +using System.Collections.Specialized; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Processes; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IImportScript + { + public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalTrack localTrack, TrackFile trackFile, TransferMode mode); + } + + public class ImportScriptService : IImportScript + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IProcessProvider _processProvider; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public ImportScriptService(IProcessProvider processProvider, + IConfigService configService, + IConfigFileProvider configFileProvider, + Logger logger) + { + _processProvider = processProvider; + _configService = configService; + _configFileProvider = configFileProvider; + _logger = logger; + } + + public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalTrack localTrack, TrackFile trackFile, TransferMode mode) + { + var artist = localTrack.Artist; + var oldFiles = localTrack.OldFiles; + var downloadClientInfo = localTrack.DownloadItem?.DownloadClientInfo; + var downloadId = localTrack.DownloadItem?.DownloadId; + + if (!_configService.UseScriptImport) + { + return ScriptImportDecision.DeferMove; + } + + var environmentVariables = new StringDictionary(); + + environmentVariables.Add("Lidarr_SourcePath", sourcePath); + environmentVariables.Add("Lidarr_DestinationPath", destinationFilePath); + + environmentVariables.Add("Lidarr_InstanceName", _configFileProvider.InstanceName); + environmentVariables.Add("Lidarr_ApplicationUrl", _configService.ApplicationUrl); + environmentVariables.Add("Lidarr_TransferMode", mode.ToString()); + + environmentVariables.Add("Lidarr_Artist_Id", artist.Id.ToString()); + environmentVariables.Add("Lidarr_Artist_Name", artist.Metadata.Value.Name); + environmentVariables.Add("Lidarr_Artist_Path", artist.Path); + environmentVariables.Add("Lidarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId); + environmentVariables.Add("Lidarr_Artist_Type", artist.Metadata.Value.Type); + + environmentVariables.Add("Lidarr_Album_Id", localTrack.Album.Id.ToString()); + environmentVariables.Add("Lidarr_Album_Title", localTrack.Album.Title); + environmentVariables.Add("Lidarr_Album_Overview", localTrack.Album.Overview); + environmentVariables.Add("Lidarr_Album_MBId", localTrack.Album.ForeignAlbumId); + environmentVariables.Add("Lidarr_AlbumRelease_MBId", localTrack.Release.ForeignReleaseId); + environmentVariables.Add("Lidarr_Album_ReleaseDate", localTrack.Release.ReleaseDate.ToString()); + + environmentVariables.Add("Lidarr_Download_Client", downloadClientInfo?.Name ?? string.Empty); + environmentVariables.Add("Lidarr_Download_Client_Type", downloadClientInfo?.Type ?? string.Empty); + environmentVariables.Add("Lidarr_Download_Id", downloadId ?? string.Empty); + + if (oldFiles.Any()) + { + environmentVariables.Add("Lidarr_DeletedPaths", string.Join("|", oldFiles.Select(e => e.Path))); + environmentVariables.Add("Lidarr_DeletedDateAdded", string.Join("|", oldFiles.Select(e => e.DateAdded))); + } + + _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 = _videoFileInfoReader.GetMediaInfo(destinationFilePath); + 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 95519482f..44266e888 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IDiskProvider _diskProvider; private readonly IRootFolderWatchingService _rootFolderWatchingService; private readonly IMediaFileAttributeService _mediaFileAttributeService; + private readonly IImportScript _scriptImportDecider; private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly Logger _logger; @@ -43,6 +44,7 @@ namespace NzbDrone.Core.MediaFiles IDiskProvider diskProvider, IRootFolderWatchingService rootFolderWatchingService, IMediaFileAttributeService mediaFileAttributeService, + IImportScript scriptImportDecider, IEventAggregator eventAggregator, IConfigService configService, Logger logger) @@ -55,6 +57,7 @@ namespace NzbDrone.Core.MediaFiles _diskProvider = diskProvider; _rootFolderWatchingService = rootFolderWatchingService; _mediaFileAttributeService = mediaFileAttributeService; + _scriptImportDecider = scriptImportDecider; _eventAggregator = eventAggregator; _configService = configService; _logger = logger; @@ -63,6 +66,11 @@ namespace NzbDrone.Core.MediaFiles public TrackFile MoveTrackFile(TrackFile trackFile, Artist artist) { var tracks = _trackService.GetTracksByFileId(trackFile.Id); + return MoveTrackFile(trackFile, artist, tracks); + } + + private TrackFile MoveTrackFile(TrackFile trackFile, Artist artist, List tracks) + { var album = _albumService.GetAlbum(trackFile.AlbumId); var newFileName = _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile); var filePath = _buildFileNames.BuildTrackFilePath(artist, newFileName, Path.GetExtension(trackFile.Path)); @@ -83,7 +91,7 @@ namespace NzbDrone.Core.MediaFiles _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) @@ -96,14 +104,14 @@ namespace NzbDrone.Core.MediaFiles if (_configService.CopyUsingHardlinks) { _logger.Debug("Hardlinking 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 tracks, string destinationFilePath, TransferMode mode) + private TrackFile TransferFile(TrackFile trackFile, Artist artist, List tracks, string destinationFilePath, TransferMode mode, LocalTrack localTrack = null) { Ensure.That(trackFile, () => trackFile).IsNotNull(); Ensure.That(artist, () => artist).IsNotNull(); @@ -122,10 +130,34 @@ namespace NzbDrone.Core.MediaFiles } _rootFolderWatchingService.ReportFileSystemChangeBeginning(trackFilePath, destinationFilePath); - _diskTransferService.TransferFile(trackFilePath, destinationFilePath, mode); + + var transfer = true; trackFile.Path = destinationFilePath; + if (localTrack is not null) + { + var scriptImportDecision = _scriptImportDecider.TryImport(trackFilePath, destinationFilePath, localTrack, trackFile, mode); + + switch (scriptImportDecision) + { + case ScriptImportDecision.DeferMove: + break; + case ScriptImportDecision.RenameRequested: + MoveTrackFile(trackFile, artist, trackFile.Tracks); + transfer = false; + break; + case ScriptImportDecision.MoveComplete: + transfer = false; + break; + } + } + + if (transfer) + { + _diskTransferService.TransferFile(trackFilePath, destinationFilePath, mode); + } + _updateTrackFileService.ChangeFileDateForFile(trackFile, artist, tracks); try diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index 7988edda8..1c57e3461 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -216,8 +216,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport trackFile.SceneName = localTrack.SceneName; trackFile.OriginalFilePath = GetOriginalFilePath(downloadClientItem, localTrack); - var moveResult = _trackFileUpgrader.UpgradeTrackFile(trackFile, localTrack, copyOnly); - oldFiles = moveResult.OldFiles; + oldFiles = _trackFileUpgrader.UpgradeTrackFile(trackFile, localTrack, copyOnly).OldFiles; } else { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 4ad968ea8..3341c892e 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -108,6 +108,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var localTrack = new LocalTrack { DownloadClientAlbumInfo = downloadClientItemInfo, + DownloadItem = downloadClientItem, FolderAlbumInfo = folderInfo, Path = file.FullName, Size = file.Length, diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 0dd3c3e62..f2a94292c 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -70,6 +70,8 @@ namespace NzbDrone.Core.MediaFiles _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); } + localTrack.OldFiles = moveFileResult.OldFiles; + if (copyOnly) { moveFileResult.TrackFile = _trackFileMover.CopyTrackFile(trackFile, localTrack); diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 1fc38c6ef..741ef9d06 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Identification; using NzbDrone.Core.Music; using NzbDrone.Core.Qualities; @@ -19,11 +21,13 @@ namespace NzbDrone.Core.Parser.Model public ParsedTrackInfo FileTrackInfo { get; set; } public ParsedAlbumInfo FolderAlbumInfo { get; set; } public ParsedAlbumInfo DownloadClientAlbumInfo { get; set; } + public DownloadClientItem DownloadItem { get; set; } public List AcoustIdResults { get; set; } public Artist Artist { get; set; } public Album Album { get; set; } public AlbumRelease Release { get; set; } public List Tracks { get; set; } + public List OldFiles { get; set; } public Distance Distance { get; set; } public QualityModel Quality { get; set; } public bool ExistingFile { get; set; }