New: Option to Import via Script

Closes Sonarr/Sonarr#791

(cherry picked from commit 9f1e2151206a077334a9c34a12a373b465752d87)
This commit is contained in:
JeWe37 2023-05-23 05:36:17 +02:00 committed by bakerboy448
parent f627a3cb88
commit bed907e720
11 changed files with 278 additions and 18 deletions

View file

@ -124,29 +124,29 @@ class MediaManagement extends Component {
<NamingConnector />
{
isFetching &&
isFetching ?
<FieldSet legend={translate('NamingSettings')}>
<LoadingIndicator />
</FieldSet>
</FieldSet> : null
}
{
!isFetching && error &&
!isFetching && error ?
<FieldSet legend={translate('NamingSettings')}>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadMediaManagementSettings')}
</Alert>
</FieldSet>
</FieldSet> : null
}
{
hasSettings && !isFetching && !error &&
hasSettings && !isFetching && !error ?
<Form
id="mediaManagementSettings"
{...otherProps}
>
{
advancedSettings &&
advancedSettings ?
<FieldSet legend={translate('Folders')}>
<FormGroup
advancedSettings={advancedSettings}
@ -183,11 +183,11 @@ class MediaManagement extends Component {
{...settings.deleteEmptyFolders}
/>
</FormGroup>
</FieldSet>
</FieldSet> : null
}
{
advancedSettings &&
advancedSettings ?
<FieldSet
legend={translate('Importing')}
>
@ -245,6 +245,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('ImportUsingScriptHelpText')}
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('ImportScriptPathHelpText')}
onChange={onInputChange}
{...settings.scriptImportPath}
/>
</FormGroup> : null
}
<FormGroup size={sizes.MEDIUM}>
<FormLabel>
{translate('ImportExtraFiles')}
@ -279,7 +314,7 @@ class MediaManagement extends Component {
/>
</FormGroup> : null
}
</FieldSet>
</FieldSet> : null
}
<FieldSet
@ -424,7 +459,7 @@ class MediaManagement extends Component {
</FieldSet>
{
advancedSettings && !isWindows &&
advancedSettings && !isWindows ?
<FieldSet
legend={translate('Permissions')}
>
@ -483,9 +518,9 @@ class MediaManagement extends Component {
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet>
</FieldSet> : null
}
</Form>
</Form> : null
}
</PageContentBody>
</PageContent>

View file

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

View file

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

View file

@ -10870,6 +10870,12 @@
"copyUsingHardlinks": {
"type": "boolean"
},
"useScriptImport": {
"type": "boolean"
},
"scriptImportPath": {
"type": "string"
},
"importExtraFiles": {
"type": "boolean"
},

View file

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

View file

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

View file

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

View file

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

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

@ -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<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();
@ -123,8 +126,31 @@ private TrackFile TransferFile(TrackFile trackFile, Artist artist, List<Track> t
throw new SameFilenameException("File not moved, source and destination are the same", trackFilePath);
}
_rootFolderWatchingService.ReportFileSystemChangeBeginning(trackFilePath, destinationFilePath);
_diskTransferService.TransferFile(trackFilePath, destinationFilePath, mode);
var transfer = true;
if (localTrack is not null)
{
var scriptImportDecision = _scriptImportDecider.TryImport(trackFilePath, destinationFilePath, localTrack, trackFile, mode, null);
switch (scriptImportDecision)
{
case ScriptImportDecision.DeferMove:
break;
case ScriptImportDecision.RenameRequested:
MoveTrackFile(trackFile, artist);
transfer = false;
break;
case ScriptImportDecision.MoveComplete:
transfer = false;
break;
}
}
if (transfer)
{
_rootFolderWatchingService.ReportFileSystemChangeBeginning(trackFilePath, destinationFilePath);
_diskTransferService.TransferFile(trackFilePath, destinationFilePath, mode);
}
trackFile.Path = destinationFilePath;