New: Option to Import via Script

(cherry picked from commit 9f1e2151206a077334a9c34a12a373b465752d87)
This commit is contained in:
JeWe37 2023-05-23 05:36:17 +02:00 committed by Bogdan
parent f38f00c64e
commit c8a950eb40
14 changed files with 254 additions and 21 deletions

View File

@ -68,29 +68,29 @@ class MediaManagement extends Component {
<NamingConnector />
{
isFetching &&
isFetching ?
<FieldSet legend={translate('NamingSettings')}>
<LoadingIndicator />
</FieldSet>
</FieldSet> : null
}
{
!isFetching && error &&
!isFetching && error ?
<FieldSet legend={translate('NamingSettings')}>
<div>
{translate('UnableToLoadMediaManagementSettings')}
</div>
</FieldSet>
</FieldSet> : null
}
{
hasSettings && !isFetching && !error &&
hasSettings && !isFetching && !error ?
<Form
id="mediaManagementSettings"
{...otherProps}
>
{
advancedSettings &&
advancedSettings ?
<FieldSet legend={translate('Folders')}>
<FormGroup
advancedSettings={advancedSettings}
@ -127,11 +127,11 @@ class MediaManagement extends Component {
{...settings.deleteEmptyFolders}
/>
</FormGroup>
</FieldSet>
</FieldSet> : null
}
{
advancedSettings &&
advancedSettings ?
<FieldSet
legend={translate('Importing')}
>
@ -194,6 +194,41 @@ class MediaManagement extends Component {
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="useScriptImport"
helpText={translate('UseScriptImportHelpText')}
onChange={onInputChange}
{...settings.useScriptImport}
/>
</FormGroup>
{
settings.useScriptImport.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
includeFiles={true}
name="scriptImportPath"
helpText={translate('ScriptImportPathHelpText')}
onChange={onInputChange}
{...settings.scriptImportPath}
/>
</FormGroup> : null
}
<FormGroup size={sizes.MEDIUM}>
<FormLabel>
{translate('ImportExtraFiles')}
@ -209,7 +244,7 @@ class MediaManagement extends Component {
</FormGroup>
{
settings.importExtraFiles.value &&
settings.importExtraFiles.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
@ -228,9 +263,9 @@ class MediaManagement extends Component {
onChange={onInputChange}
{...settings.extraFileExtensions}
/>
</FormGroup>
</FormGroup> : null
}
</FieldSet>
</FieldSet> : null
}
<FieldSet
@ -375,7 +410,7 @@ class MediaManagement extends Component {
</FieldSet>
{
advancedSettings && !isWindows &&
advancedSettings && !isWindows ?
<FieldSet
legend={translate('Permissions')}
>
@ -434,9 +469,9 @@ class MediaManagement extends Component {
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet>
</FieldSet> : null
}
</Form>
</Form> : null
}
</PageContentBody>
</PageContent>

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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); }

View File

@ -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; }

View File

@ -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",

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,10 @@
namespace NzbDrone.Core.MediaFiles
{
public enum ScriptImportDecision
{
MoveComplete,
RenameRequested,
RejectExtra,
DeferMove
}
}

View File

@ -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)
{
}
}
}

View File

@ -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<Track> 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<Track> tracks, string destinationFilePath, TransferMode mode)
private TrackFile TransferFile(TrackFile trackFile, Artist artist, List<Track> 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

View File

@ -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
{

View File

@ -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,

View File

@ -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);

View File

@ -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<string> AcoustIdResults { get; set; }
public Artist Artist { get; set; }
public Album Album { get; set; }
public AlbumRelease Release { get; set; }
public List<Track> Tracks { get; set; }
public List<TrackFile> OldFiles { get; set; }
public Distance Distance { get; set; }
public QualityModel Quality { get; set; }
public bool ExistingFile { get; set; }