mirror of
https://github.com/Lidarr/Lidarr
synced 2025-12-06 00:16:41 +01:00
New: Option to Import via Script
Closes Sonarr/Sonarr#791 (cherry picked from commit 9f1e2151206a077334a9c34a12a373b465752d87)
This commit is contained in:
parent
f627a3cb88
commit
bed907e720
11 changed files with 278 additions and 18 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10870,6 +10870,12 @@
|
|||
"copyUsingHardlinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"useScriptImport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"scriptImportPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"importExtraFiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
125
src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs
Normal file
125
src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/NzbDrone.Core/MediaFiles/ScriptImportDecision.cs
Normal file
10
src/NzbDrone.Core/MediaFiles/ScriptImportDecision.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public enum ScriptImportDecision
|
||||
{
|
||||
MoveComplete,
|
||||
RenameRequested,
|
||||
RejectExtra,
|
||||
DeferMove
|
||||
}
|
||||
}
|
||||
23
src/NzbDrone.Core/MediaFiles/ScriptImportException.cs
Normal file
23
src/NzbDrone.Core/MediaFiles/ScriptImportException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue