You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Lidarr/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs

234 lines
9.6 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
namespace NzbDrone.Core.MediaFiles.TrackImport
{
public interface IMakeImportDecision
{
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease);
}
public class ImportDecisionMaker : IMakeImportDecision
{
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> _trackSpecifications;
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumSpecifications;
private readonly IMediaFileService _mediaFileService;
private readonly IAudioTagService _audioTagService;
private readonly IAugmentingService _augmentingService;
private readonly IIdentificationService _identificationService;
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> trackSpecifications,
IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> albumSpecifications,
IMediaFileService mediaFileService,
IAudioTagService audioTagService,
IAugmentingService augmentingService,
IIdentificationService identificationService,
IAlbumService albumService,
IReleaseService releaseService,
IEventAggregator eventAggregator,
IDiskProvider diskProvider,
Logger logger)
{
_trackSpecifications = trackSpecifications;
_albumSpecifications = albumSpecifications;
_mediaFileService = mediaFileService;
_audioTagService = audioTagService;
_augmentingService = augmentingService;
_identificationService = identificationService;
_albumService = albumService;
_releaseService = releaseService;
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_logger = logger;
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist)
{
return GetImportDecisions(musicFiles, artist, null, null, null, false, false, false);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
{
return GetImportDecisions(musicFiles, artist, null, null, folderInfo, false, true, false);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
var files = filterExistingFiles && (artist != null) ? _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist) : musicFiles.ToList();
_logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count);
ParsedAlbumInfo downloadClientItemInfo = null;
if (downloadClientItem != null)
{
downloadClientItemInfo = Parser.Parser.ParseAlbumTitle(downloadClientItem.Title);
}
var localTracks = new List<LocalTrack>();
var decisions = new List<ImportDecision<LocalTrack>>();
foreach (var file in files)
{
var localTrack = new LocalTrack
{
Artist = artist,
Album = album,
DownloadClientAlbumInfo = downloadClientItemInfo,
FolderTrackInfo = folderInfo,
Path = file,
FileTrackInfo = _audioTagService.ReadTags(file)
};
try
{
// TODO fix otherfiles?
_augmentingService.Augment(localTrack, true);
localTracks.Add(localTrack);
}
catch (AugmentingFailedException)
{
decisions.Add(new ImportDecision<LocalTrack>(localTrack, new Rejection("Unable to parse file")));
}
catch (Exception e)
{
_logger.Error(e, "Couldn't import file. {0}", localTrack.Path);
decisions.Add(new ImportDecision<LocalTrack>(localTrack, new Rejection("Unexpected error processing file")));
}
}
_logger.Debug($"Tags parsed for {files.Count} files in {watch.ElapsedMilliseconds}ms");
var releases = _identificationService.Identify(localTracks, artist, album, null, newDownload, singleRelease);
foreach (var release in releases)
{
release.NewDownload = newDownload;
var releaseDecision = GetDecision(release);
foreach (var localTrack in release.LocalTracks)
{
if (releaseDecision.Approved)
{
decisions.AddIfNotNull(GetDecision(localTrack));
}
else
{
decisions.Add(new ImportDecision<LocalTrack>(localTrack, releaseDecision.Rejections.ToArray()));
}
}
}
return decisions;
}
private ImportDecision<LocalAlbumRelease> GetDecision(LocalAlbumRelease localAlbumRelease)
{
ImportDecision<LocalAlbumRelease> decision = null;
if (localAlbumRelease.AlbumRelease == null)
{
decision = new ImportDecision<LocalAlbumRelease>(localAlbumRelease, new Rejection($"Couldn't find similar album for {localAlbumRelease}"));
}
else
{
var reasons = _albumSpecifications.Select(c => EvaluateSpec(c, localAlbumRelease))
.Where(c => c != null);
decision = new ImportDecision<LocalAlbumRelease>(localAlbumRelease, reasons.ToArray());
}
if (decision == null)
{
_logger.Error("Unable to make a decision on {0}", localAlbumRelease);
}
else if (decision.Rejections.Any())
{
_logger.Debug("Album rejected for the following reasons: {0}", string.Join(", ", decision.Rejections));
}
else
{
_logger.Debug("Album accepted");
}
return decision;
}
private ImportDecision<LocalTrack> GetDecision(LocalTrack localTrack)
{
ImportDecision<LocalTrack> decision = null;
if (localTrack.Tracks.Empty())
{
decision = localTrack.Album != null ? new ImportDecision<LocalTrack>(localTrack, new Rejection($"Couldn't parse track from: {localTrack.FileTrackInfo}")) :
new ImportDecision<LocalTrack>(localTrack, new Rejection($"Couldn't parse album from: {localTrack.FileTrackInfo}"));
}
else
{
var reasons = _trackSpecifications.Select(c => EvaluateSpec(c, localTrack))
.Where(c => c != null);
decision = new ImportDecision<LocalTrack>(localTrack, reasons.ToArray());
}
if (decision == null)
{
_logger.Error("Unable to make a decision on {0}", localTrack.Path);
}
else if (decision.Rejections.Any())
{
_logger.Debug("File rejected for the following reasons: {0}", string.Join(", ", decision.Rejections));
}
else
{
_logger.Debug("File accepted");
}
return decision;
}
private Rejection EvaluateSpec<T>(IImportDecisionEngineSpecification<T> spec, T item)
{
try
{
var result = spec.IsSatisfiedBy(item);
if (!result.Accepted)
{
return new Rejection(result.Reason);
}
}
catch (Exception e)
{
_logger.Error(e, "Couldn't evaluate decision on {0}", item);
return new Rejection($"{spec.GetType().Name}: {e.Message}");
}
return null;
}
}
}