UI Updates (Cancel Import, Move Artist, Manual Import from Artist)

Ability to cancel an import lookup/search at any point.
Ability to move artist path from Artist Edit or bulk move from Mass Editor.
Trigger manual import for Artist path from Artist Detail page.
Pulled from Sonarr
This commit is contained in:
Qstick 2017-12-29 22:23:04 -05:00
commit d8c89f5bbd
79 changed files with 1075 additions and 376 deletions

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using Nancy;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using Lidarr.Http.Extensions;
namespace Lidarr.Api.V1.Artist
@ -9,11 +12,13 @@ namespace Lidarr.Api.V1.Artist
public class ArtistEditorModule : LidarrV1Module
{
private readonly IArtistService _artistService;
private readonly IManageCommandQueue _commandQueueManager;
public ArtistEditorModule(IArtistService artistService)
public ArtistEditorModule(IArtistService artistService, IManageCommandQueue commandQueueManager)
: base("/artist/editor")
{
_artistService = artistService;
_commandQueueManager = commandQueueManager;
Put["/"] = artist => SaveAll();
Delete["/"] = artist => DeleteArtist();
}
@ -22,6 +27,7 @@ namespace Lidarr.Api.V1.Artist
{
var resource = Request.Body.FromJson<ArtistEditorResource>();
var artistToUpdate = _artistService.GetArtists(resource.ArtistIds);
var artistToMove = new List<BulkMoveArtist>();
foreach (var artist in artistToUpdate)
{
@ -53,6 +59,12 @@ namespace Lidarr.Api.V1.Artist
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
{
artist.RootFolderPath = resource.RootFolderPath;
artistToMove.Add(new BulkMoveArtist
{
ArtistId = artist.Id,
SourcePath = artist.Path
});
}
if (resource.Tags != null)
@ -75,6 +87,15 @@ namespace Lidarr.Api.V1.Artist
}
}
if (resource.MoveFiles && artistToMove.Any())
{
_commandQueueManager.Push(new BulkMoveArtistCommand
{
DestinationRootFolder = resource.RootFolderPath,
Artist = artistToMove
});
}
return _artistService.UpdateArtists(artistToUpdate)
.ToResource()
.AsResponse(HttpStatusCode.Accepted);

View file

@ -14,6 +14,7 @@ namespace Lidarr.Api.V1.Artist
public string RootFolderPath { get; set; }
public List<int> Tags { get; set; }
public ApplyTags ApplyTags { get; set; }
public bool MoveFiles { get; set; }
}
public enum ApplyTags

View file

@ -7,9 +7,11 @@ using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
@ -35,12 +37,14 @@ namespace Lidarr.Api.V1.Artist
private readonly IAddArtistService _addArtistService;
private readonly IArtistStatisticsService _artistStatisticsService;
private readonly IMapCoversToLocal _coverMapper;
private readonly IManageCommandQueue _commandQueueManager;
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
IArtistService artistService,
IAddArtistService addArtistService,
IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper,
IManageCommandQueue commandQueueManager,
RootFolderValidator rootFolderValidator,
ArtistPathValidator artistPathValidator,
ArtistExistsValidator artistExistsValidator,
@ -57,6 +61,7 @@ namespace Lidarr.Api.V1.Artist
_artistStatisticsService = artistStatisticsService;
_coverMapper = coverMapper;
_commandQueueManager = commandQueueManager;
GetResourceAll = AllArtists;
GetResourceById = GetArtist;
@ -127,7 +132,24 @@ namespace Lidarr.Api.V1.Artist
private void UpdateArtist(ArtistResource artistResource)
{
var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id));
var moveFiles = Request.GetBooleanQueryParameter("moveFiles");
var artist = _artistService.GetArtist(artistResource.Id);
if (moveFiles)
{
var sourcePath = artist.Path;
var destinationPath = artistResource.Path;
_commandQueueManager.Push(new MoveArtistCommand
{
ArtistId = artist.Id,
SourcePath = sourcePath,
DestinationPath = destinationPath,
Trigger = CommandTrigger.Manual
});
}
var model = artistResource.ToModel(artist);
_artistService.UpdateArtist(model);

View file

@ -55,10 +55,8 @@ namespace Lidarr.Api.V1.History
if (model.Artist != null)
{
resource.QualityCutoffNotMet = _upgradableSpecification.CutoffNotMet(model.Artist.Profile.Value,
model.Artist.LanguageProfile,
model.Quality,
model.Language);
resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Artist.Profile.Value, model.Quality);
resource.LanguageCutoffNotMet = _upgradableSpecification.LanguageCutoffNotMet(model.Artist.LanguageProfile, model.Language);
}
return resource;

View file

@ -19,6 +19,7 @@ namespace Lidarr.Api.V1.History
public Language Language { get; set; }
public QualityModel Quality { get; set; }
public bool QualityCutoffNotMet { get; set; }
public bool LanguageCutoffNotMet { get; set; }
public DateTime Date { get; set; }
public string DownloadId { get; set; }

View file

@ -3,6 +3,8 @@ using System.Linq;
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
using NzbDrone.Core.Qualities;
using Lidarr.Http;
using Lidarr.Http.Extensions;
namespace Lidarr.Api.V1.ManualImport
{
@ -22,8 +24,9 @@ namespace Lidarr.Api.V1.ManualImport
{
var folder = (string)Request.Query.folder;
var downloadId = (string)Request.Query.downloadId;
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
return _manualImportService.GetMediaFiles(folder, downloadId).ToResource().Select(AddQualityWeight).ToList();
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
}
private ManualImportResource AddQualityWeight(ManualImportResource item)

View file

@ -22,6 +22,8 @@ namespace Lidarr.Api.V1.TrackFiles
public MediaInfoResource MediaInfo { get; set; }
public bool QualityCutoffNotMet { get; set; }
public bool LanguageCutoffNotMet { get; set; }
}
public static class TrackFileResourceMapper
@ -67,11 +69,9 @@ namespace Lidarr.Api.V1.TrackFiles
Language = model.Language,
Quality = model.Quality,
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(artist.Profile.Value,
artist.LanguageProfile.Value,
model.Quality,
model.Language)
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.Profile.Value, model.Quality),
LanguageCutoffNotMet = upgradableSpecification.LanguageCutoffNotMet(artist.LanguageProfile.Value, model.Language)
};
}
}

View file

@ -1,4 +1,6 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
@ -16,6 +18,7 @@ namespace NzbDrone.Core.Test.MusicTests
{
private Artist _artist;
private MoveArtistCommand _command;
private BulkMoveArtistCommand _bulkCommand;
[SetUp]
public void Setup()
@ -31,6 +34,19 @@ namespace NzbDrone.Core.Test.MusicTests
DestinationPath = @"C:\Test\Music2\Artist".AsOsAgnostic()
};
_bulkCommand = new BulkMoveArtistCommand
{
Artist = new List<BulkMoveArtist>
{
new BulkMoveArtist
{
ArtistId = 1,
SourcePath = @"C:\Test\Music\Artist".AsOsAgnostic()
}
},
DestinationRootFolder = @"C:\Test\Music2".AsOsAgnostic()
};
Mocker.GetMock<IArtistService>()
.Setup(s => s.GetArtist(It.IsAny<int>()))
.Returns(_artist);
@ -48,52 +64,52 @@ namespace NzbDrone.Core.Test.MusicTests
{
GivenFailedMove();
Assert.Throws<IOException>(() => Subject.Execute(_command));
Subject.Execute(_command);
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_no_update_artist_path_on_error()
public void should_revert_artist_path_on_error()
{
GivenFailedMove();
Assert.Throws<IOException>(() => Subject.Execute(_command));
Subject.Execute(_command);
ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Never());
.Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Once());
}
[Test]
public void should_use_destination_path()
{
Subject.Execute(_command);
Mocker.GetMock<IDiskTransferService>()
.Verify(v => v.TransferFolder(_command.SourcePath, _command.DestinationPath, TransferMode.Move, It.IsAny<bool>()), Times.Once());
Mocker.GetMock<IBuildFileNames>()
.Verify(v => v.GetArtistFolder(It.IsAny<Artist>(), null), Times.Never());
}
[Test]
public void should_build_new_path_when_root_folder_is_provided()
{
_command.DestinationPath = null;
_command.DestinationRootFolder = @"C:\Test\Music3".AsOsAgnostic();
var expectedPath = @"C:\Test\Music3\Artist".AsOsAgnostic();
var artistFolder = "Artist";
var expectedPath = Path.Combine(_bulkCommand.DestinationRootFolder, artistFolder);
Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.GetArtistFolder(It.IsAny<Artist>(), null))
.Returns("Artist");
.Setup(s => s.GetArtistFolder(It.IsAny<Artist>(), null))
.Returns(artistFolder);
Subject.Execute(_command);
Subject.Execute(_bulkCommand);
Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Path == expectedPath)), Times.Once());
}
[Test]
public void should_use_destination_path_if_destination_root_folder_is_blank()
{
Subject.Execute(_command);
Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Path == _command.DestinationPath)), Times.Once());
Mocker.GetMock<IBuildFileNames>()
.Verify(v => v.GetArtistFolder(It.IsAny<Artist>(), null), Times.Never());
Mocker.GetMock<IDiskTransferService>()
.Verify(v => v.TransferFolder(_bulkCommand.Artist.First().SourcePath, expectedPath, TransferMode.Move, It.IsAny<bool>()), Times.Once());
}
}
}

View file

@ -9,6 +9,8 @@ namespace NzbDrone.Core.DecisionEngine
public interface IUpgradableSpecification
{
bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage);
bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null);
bool LanguageCutoffNotMet(LanguageProfile languageProfile, Language currentLanguage);
bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null);
bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality);
}
@ -68,29 +70,46 @@ namespace NzbDrone.Core.DecisionEngine
return true;
}
public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null)
public bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null)
{
var languageCompare = new LanguageComparer(languageProfile).Compare(currentLanguage, languageProfile.Cutoff);
var qualityCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality.Id, profile.Cutoff);
// If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language
if (languageCompare < 0)
if (qualityCompare < 0)
{
return true;
}
if (qualityCompare >= 0)
if (qualityCompare == 0 && newQuality != null && IsRevisionUpgrade(currentQuality, newQuality))
{
if (newQuality != null && IsRevisionUpgrade(currentQuality, newQuality))
{
return true;
}
_logger.Debug("Existing item meets cut-off. skipping.");
return false;
return true;
}
return true;
return false;
}
public bool LanguageCutoffNotMet(LanguageProfile languageProfile, Language currentLanguage)
{
var languageCompare = new LanguageComparer(languageProfile).Compare(currentLanguage, languageProfile.Cutoff);
return languageCompare < 0;
}
public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null)
{
// If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language
if (LanguageCutoffNotMet(languageProfile, currentLanguage))
{
return true;
}
if (QualityCutoffNotMet(profile, currentQuality, newQuality))
{
return true;
}
_logger.Debug("Existing item meets cut-off. skipping.");
return false;
}
public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality)

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
public RenameArtistCommand()
{
ArtistIds = new List<int>();
}
}
}
}

View file

@ -19,6 +19,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
{
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist);
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo);
List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo, bool filterExistingFiles);
}
public class ImportDecisionMaker : IMakeImportDecision
@ -52,14 +54,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
{
var newFiles = _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist);
return GetImportDecisions(musicFiles, artist, folderInfo, false);
}
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, musicFiles.Count());
public List<ImportDecision> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo, bool filterExistingFiles)
{
var files = filterExistingFiles ? _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist) : musicFiles.ToList();
_logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count);
var shouldUseFolderName = ShouldUseFolderName(musicFiles, artist, folderInfo);
var decisions = new List<ImportDecision>();
foreach (var file in newFiles)
foreach (var file in files)
{
decisions.AddIfNotNull(GetDecision(file, artist, folderInfo, shouldUseFolderName));
}

View file

@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
public interface IManualImportService
{
List<ManualImportItem> GetMediaFiles(string path, string downloadId);
List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles);
}
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -68,7 +68,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
_logger = logger;
}
public List<ManualImportItem> GetMediaFiles(string path, string downloadId)
public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles)
{
if (downloadId.IsNotNullOrWhiteSpace())
{
@ -92,10 +92,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
return new List<ManualImportItem> { ProcessFile(path, downloadId) };
}
return ProcessFolder(path, downloadId);
return ProcessFolder(path, downloadId, filterExistingFiles);
}
private List<ManualImportItem> ProcessFolder(string folder, string downloadId)
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, bool filterExistingFiles)
{
var directoryInfo = new DirectoryInfo(folder);
var artist = _parsingService.GetArtist(directoryInfo.Name);
@ -115,7 +115,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name);
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, folderInfo);
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, folderInfo, filterExistingFiles);
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
}

View file

@ -140,8 +140,11 @@ namespace NzbDrone.Core.Music
_logger.Trace("Updating: {0}", s.Name);
if (!s.RootFolderPath.IsNullOrWhiteSpace())
{
var folderName = new DirectoryInfo(s.Path).Name;
s.Path = Path.Combine(s.RootFolderPath, folderName);
// Build the artist folder name instead of using the existing folder name.
// This may lead to folder name changes, but consistent with adding a new artist.
s.Path = Path.Combine(s.RootFolderPath, _fileNameBuilder.GetArtistFolder(s));
_logger.Trace("Changing path for {0} to {1}", s.Name, s.Path);
}

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Music.Commands
{
public class BulkMoveArtistCommand : Command
{
public List<BulkMoveArtist> Artist { get; set; }
public string DestinationRootFolder { get; set; }
public override bool SendUpdatesToClient => true;
}
public class BulkMoveArtist
{
public int ArtistId { get; set; }
public string SourcePath { get; set; }
}
}

View file

@ -1,4 +1,4 @@
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Music.Commands
{
@ -7,6 +7,7 @@ namespace NzbDrone.Core.Music.Commands
public int ArtistId { get; set; }
public string SourcePath { get; set; }
public string DestinationPath { get; set; }
public string DestinationRootFolder { get; set; }
public override bool SendUpdatesToClient => true;
}
}

View file

@ -1,7 +1,6 @@
using System.IO;
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@ -11,7 +10,7 @@ using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.Music
{
public class MoveArtistService : IExecute<MoveArtistCommand>
public class MoveArtistService : IExecute<MoveArtistCommand>, IExecute<BulkMoveArtistCommand>
{
private readonly IArtistService _artistService;
private readonly IBuildFileNames _filenameBuilder;
@ -32,38 +31,56 @@ namespace NzbDrone.Core.Music
_logger = logger;
}
public void Execute(MoveArtistCommand message)
private void MoveSingleArtist(Artist artist, string sourcePath, string destinationPath)
{
var artist = _artistService.GetArtist(message.ArtistId);
var source = message.SourcePath;
var destination = message.DestinationPath;
_logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath);
if (!message.DestinationRootFolder.IsNullOrWhiteSpace())
{
_logger.Debug("Buiding destination path using root folder: {0} and the artist name", message.DestinationRootFolder);
destination = Path.Combine(message.DestinationRootFolder, _filenameBuilder.GetArtistFolder(artist));
}
_logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, source, destination);
//TODO: Move to transactional disk operations
try
{
_diskTransferService.TransferFolder(source, destination, TransferMode.Move);
_diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move);
}
catch (IOException ex)
{
_logger.Error(ex, "Unable to move artist from '{0}' to '{1}'", source, destination);
throw;
_logger.Error(ex, "Unable to move artist from '{0}' to '{1}'. Try moving files manually", sourcePath, destinationPath);
RevertPath(artist.Id, sourcePath);
}
_logger.ProgressInfo("{0} moved successfully to {1}", artist.Name, artist.Path);
//Update the artist path to the new path
artist.Path = destination;
artist = _artistService.UpdateArtist(artist);
_eventAggregator.PublishEvent(new ArtistMovedEvent(artist, sourcePath, destinationPath));
}
_eventAggregator.PublishEvent(new ArtistMovedEvent(artist, source, destination));
private void RevertPath(int artistId, string path)
{
var artist = _artistService.GetArtist(artistId);
artist.Path = path;
_artistService.UpdateArtist(artist);
}
public void Execute(MoveArtistCommand message)
{
var artist = _artistService.GetArtist(message.ArtistId);
MoveSingleArtist(artist, message.SourcePath, message.DestinationPath);
}
public void Execute(BulkMoveArtistCommand message)
{
var artistToMove = message.Artist;
var destinationRootFolder = message.DestinationRootFolder;
_logger.ProgressInfo("Moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
foreach (var s in artistToMove)
{
var artist = _artistService.GetArtist(s.ArtistId);
var destinationPath = Path.Combine(destinationRootFolder, _filenameBuilder.GetArtistFolder(artist));
MoveSingleArtist(artist, s.SourcePath, destinationPath);
}
_logger.ProgressInfo("Finished moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
}
}
}

View file

@ -758,6 +758,7 @@
<Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="Music\ArtistStatusType.cs" />
<Compile Include="Music\AlbumCutoffService.cs" />
<Compile Include="Music\Commands\BulkMoveArtistCommand.cs" />
<Compile Include="Music\SecondaryAlbumType.cs" />
<Compile Include="Music\PrimaryAlbumType.cs" />
<Compile Include="Music\Links.cs" />