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

299 lines
12 KiB

using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles.TrackImport
{
public interface IMakeImportDecision
{
List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config);
}
public class IdentificationOverrides
{
public Artist Artist { get; set; }
public Album Album { get; set; }
public AlbumRelease AlbumRelease { get; set; }
}
public class ImportDecisionMakerInfo
{
public DownloadClientItem DownloadClientItem { get; set; }
public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
}
public class ImportDecisionMakerConfig
{
public FilterFilesType Filter { get; set; }
public bool NewDownload { get; set; }
public bool SingleRelease { get; set; }
public bool IncludeExisting { get; set; }
public bool AddNewArtists { get; set; }
}
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 IRootFolderService _rootFolderService;
private readonly IQualityProfileService _qualityProfileService;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> trackSpecifications,
IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> albumSpecifications,
IMediaFileService mediaFileService,
IAudioTagService audioTagService,
IAugmentingService augmentingService,
IIdentificationService identificationService,
IRootFolderService rootFolderService,
IQualityProfileService qualityProfileService,
Logger logger)
{
_trackSpecifications = trackSpecifications;
_albumSpecifications = albumSpecifications;
_mediaFileService = mediaFileService;
_audioTagService = audioTagService;
_augmentingService = augmentingService;
_identificationService = identificationService;
_rootFolderService = rootFolderService;
_qualityProfileService = qualityProfileService;
_logger = logger;
}
public Tuple<List<LocalTrack>, List<ImportDecision<LocalTrack>>> GetLocalTracks(List<IFileInfo> musicFiles, DownloadClientItem downloadClientItem, ParsedAlbumInfo folderInfo, FilterFilesType filter)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
var files = _mediaFileService.FilterUnchangedFiles(musicFiles, filter);
var localTracks = new List<LocalTrack>();
var decisions = new List<ImportDecision<LocalTrack>>();
_logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count);
if (!files.Any())
{
return Tuple.Create(localTracks, decisions);
}
ParsedAlbumInfo downloadClientItemInfo = null;
if (downloadClientItem != null)
{
downloadClientItemInfo = Parser.Parser.ParseAlbumTitle(downloadClientItem.Title);
}
var i = 1;
foreach (var file in files)
{
_logger.ProgressInfo($"Reading file {i++}/{files.Count}");
var localTrack = new LocalTrack
{
DownloadClientAlbumInfo = downloadClientItemInfo,
FolderAlbumInfo = folderInfo,
Path = file.FullName,
Size = file.Length,
Modified = file.LastWriteTimeUtc,
FileTrackInfo = _audioTagService.ReadTags(file.FullName),
AdditionalFile = false
};
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");
return Tuple.Create(localTracks, decisions);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config)
{
idOverrides ??= new IdentificationOverrides();
itemInfo ??= new ImportDecisionMakerInfo();
var trackData = GetLocalTracks(musicFiles, itemInfo.DownloadClientItem, itemInfo.ParsedAlbumInfo, config.Filter);
var localTracks = trackData.Item1;
var decisions = trackData.Item2;
localTracks.ForEach(x => x.ExistingFile = !config.NewDownload);
var releases = _identificationService.Identify(localTracks, idOverrides, config);
var albums = releases.GroupBy(x => x.AlbumRelease?.Album?.Value.ForeignAlbumId);
// group releases that are identified as the same album
foreach (var album in albums)
{
var albumDecisions = new List<ImportDecision<LocalAlbumRelease>>();
foreach (var release in album)
{
// make sure the appropriate quality profile is set for the release artist
// in case it's a new artist
EnsureData(release);
release.NewDownload = config.NewDownload;
albumDecisions.Add(GetDecision(release, itemInfo.DownloadClientItem));
}
// if multiple album releases accepted, reject all but one with most tracks
var acceptedReleases = albumDecisions
.Where(x => x.Approved)
.OrderByDescending(x => x.Item.TrackCount);
foreach (var decision in acceptedReleases.Skip(1))
{
decision.Reject(new Rejection("Multiple versions of an album not supported"));
}
foreach (var releaseDecision in albumDecisions)
{
foreach (var localTrack in releaseDecision.Item.LocalTracks)
{
if (releaseDecision.Approved)
{
decisions.AddIfNotNull(GetDecision(localTrack, itemInfo.DownloadClientItem));
}
else
{
decisions.Add(new ImportDecision<LocalTrack>(localTrack, releaseDecision.Rejections.ToArray()));
}
}
}
}
return decisions;
}
private void EnsureData(LocalAlbumRelease release)
{
if (release.AlbumRelease != null && release.AlbumRelease.Album.Value.Artist.Value.QualityProfileId == 0)
{
var rootFolder = _rootFolderService.GetBestRootFolder(release.LocalTracks.First().Path);
var qualityProfile = _qualityProfileService.Get(rootFolder.DefaultQualityProfileId);
var artist = release.AlbumRelease.Album.Value.Artist.Value;
artist.QualityProfileId = qualityProfile.Id;
artist.QualityProfile = qualityProfile;
}
}
private ImportDecision<LocalAlbumRelease> GetDecision(LocalAlbumRelease localAlbumRelease, DownloadClientItem downloadClientItem)
{
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, downloadClientItem))
.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, DownloadClientItem downloadClientItem)
{
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, downloadClientItem))
.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, DownloadClientItem downloadClientItem)
{
try
{
var result = spec.IsSatisfiedBy(item, downloadClientItem);
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;
}
}
}