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.
248 lines
11 KiB
248 lines
11 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using NLog;
|
|
using NzbDrone.Common.Extensions;
|
|
using NzbDrone.Common.Instrumentation;
|
|
using NzbDrone.Core.Music;
|
|
using NzbDrone.Core.Parser;
|
|
using NzbDrone.Core.Parser.Model;
|
|
|
|
namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
|
{
|
|
public static class DistanceCalculator
|
|
{
|
|
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DistanceCalculator));
|
|
|
|
public static readonly List<string> VariousArtistIds = new List<string> { "89ad4ac3-39f7-470e-963a-56509c546377" };
|
|
private static readonly List<string> VariousArtistNames = new List<string> { "various artists", "various", "va", "unknown" };
|
|
private static readonly List<IsoCountry> PreferredCountries = new List<string>
|
|
{
|
|
"United States",
|
|
"United Kingdom",
|
|
"Europe",
|
|
"[Worldwide]"
|
|
}.Select(x => IsoCountries.Find(x)).ToList();
|
|
|
|
private static bool TrackIndexIncorrect(LocalTrack localTrack, Track mbTrack, int totalTrackNumber)
|
|
{
|
|
return localTrack.FileTrackInfo.TrackNumbers[0] != mbTrack.AbsoluteTrackNumber &&
|
|
localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber;
|
|
}
|
|
|
|
private static bool TrackIndexIncorrect(CueSheet.TrackEntry cuesheetTrack, Track mbTrack, int totalTrackNumber)
|
|
{
|
|
return cuesheetTrack.Number != mbTrack.AbsoluteTrackNumber;
|
|
}
|
|
|
|
public static int GetTotalTrackNumber(Track track, List<Track> allTracks)
|
|
{
|
|
return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber);
|
|
}
|
|
|
|
public static Distance TrackDistance(LocalTrack localTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false)
|
|
{
|
|
var dist = new Distance();
|
|
|
|
var localLength = localTrack.FileTrackInfo.Duration.TotalSeconds;
|
|
var mbLength = mbTrack.Duration / 1000;
|
|
var diff = Math.Abs(localLength - mbLength) - 10;
|
|
|
|
if (mbLength > 0)
|
|
{
|
|
dist.AddRatio("track_length", diff, 30);
|
|
}
|
|
|
|
// musicbrainz never has 'featuring' in the track title
|
|
// see https://musicbrainz.org/doc/Style/Artist_Credits
|
|
dist.AddString("track_title", localTrack.FileTrackInfo.CleanTitle ?? "", mbTrack.Title);
|
|
|
|
if (includeArtist && localTrack.FileTrackInfo.ArtistTitle.IsNotNullOrWhiteSpace()
|
|
&& !VariousArtistNames.Any(x => x.Equals(localTrack.FileTrackInfo.ArtistTitle, StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
dist.AddString("track_artist", localTrack.FileTrackInfo.ArtistTitle, mbTrack.ArtistMetadata.Value.Name);
|
|
}
|
|
|
|
if (localTrack.FileTrackInfo.TrackNumbers.FirstOrDefault() > 0 && mbTrack.AbsoluteTrackNumber > 0)
|
|
{
|
|
dist.AddBool("track_index", TrackIndexIncorrect(localTrack, mbTrack, totalTrackNumber));
|
|
}
|
|
|
|
var recordingId = localTrack.FileTrackInfo.RecordingMBId;
|
|
if (recordingId.IsNotNullOrWhiteSpace())
|
|
{
|
|
dist.AddBool("recording_id", localTrack.FileTrackInfo.RecordingMBId != mbTrack.ForeignRecordingId &&
|
|
!mbTrack.OldForeignRecordingIds.Contains(localTrack.FileTrackInfo.RecordingMBId));
|
|
}
|
|
|
|
// for fingerprinted files
|
|
if (localTrack.AcoustIdResults != null)
|
|
{
|
|
dist.AddBool("recording_id", !localTrack.AcoustIdResults.Contains(mbTrack.ForeignRecordingId));
|
|
}
|
|
|
|
return dist;
|
|
}
|
|
|
|
public static Distance TrackDistance(CueSheet.TrackEntry cuesheetTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false)
|
|
{
|
|
var dist = new Distance();
|
|
|
|
// musicbrainz never has 'featuring' in the track title
|
|
// see https://musicbrainz.org/doc/Style/Artist_Credits
|
|
dist.AddString("track_title", cuesheetTrack.Title ?? "", mbTrack.Title);
|
|
|
|
if (includeArtist && cuesheetTrack.Performers.Count == 1
|
|
&& !VariousArtistNames.Any(x => x.Equals(cuesheetTrack.Performers[0], StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
dist.AddString("track_artist", cuesheetTrack.Performers[0], mbTrack.ArtistMetadata.Value.Name);
|
|
}
|
|
|
|
if (mbTrack.AbsoluteTrackNumber > 0)
|
|
{
|
|
dist.AddBool("track_index", TrackIndexIncorrect(cuesheetTrack, mbTrack, totalTrackNumber));
|
|
}
|
|
|
|
return dist;
|
|
}
|
|
|
|
public static Distance AlbumReleaseDistance(List<LocalTrack> localTracks, AlbumRelease release, TrackMapping mapping)
|
|
{
|
|
var dist = new Distance();
|
|
|
|
if (!VariousArtistIds.Contains(release.Album.Value.ArtistMetadata.Value.ForeignArtistId))
|
|
{
|
|
var artist = localTracks.MostCommon(x => x.FileTrackInfo.ArtistTitle) ?? "";
|
|
dist.AddString("artist", artist, release.Album.Value.ArtistMetadata.Value.Name);
|
|
Logger.Trace("artist: {0} vs {1}; {2}", artist, release.Album.Value.ArtistMetadata.Value.Name, dist.NormalizedDistance());
|
|
}
|
|
|
|
var title = localTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? "";
|
|
|
|
// Use the album title since the differences in release titles can cause confusion and
|
|
// aren't always correct in the tags
|
|
dist.AddString("album", title, release.Album.Value.Title);
|
|
Logger.Trace("album: {0} vs {1}; {2}", title, release.Title, dist.NormalizedDistance());
|
|
|
|
// Number of discs, either as tagged or the max disc number seen
|
|
var discCount = localTracks.MostCommon(x => x.FileTrackInfo.DiscCount);
|
|
discCount = discCount != 0 ? discCount : localTracks.Max(x => x.FileTrackInfo.DiscNumber);
|
|
if (discCount > 0)
|
|
{
|
|
dist.AddNumber("media_count", discCount, release.Media.Count);
|
|
Logger.Trace("media_count: {0} vs {1}; {2}", discCount, release.Media.Count, dist.NormalizedDistance());
|
|
}
|
|
|
|
// Media format
|
|
if (release.Media.Select(x => x.Format).Contains("Unknown"))
|
|
{
|
|
dist.Add("media_format", 1.0);
|
|
}
|
|
|
|
// Year
|
|
var localYear = localTracks.MostCommon(x => x.FileTrackInfo.Year);
|
|
if (localYear > 0 && (release.Album.Value.ReleaseDate.HasValue || release.ReleaseDate.HasValue))
|
|
{
|
|
var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0;
|
|
var releaseYear = release.ReleaseDate?.Year ?? 0;
|
|
|
|
// The single file version's year is from the album year already, to avoid false positives here we consider it's always different
|
|
var isSameWithAlbumYear = localTracks.All(x => x.IsSingleFileRelease == true) ? false : localYear == albumYear;
|
|
if (isSameWithAlbumYear || localYear == releaseYear)
|
|
{
|
|
dist.Add("year", 0.0);
|
|
}
|
|
else
|
|
{
|
|
var remoteYear = (albumYear > 0 && isSameWithAlbumYear) ? albumYear : releaseYear;
|
|
var diff = Math.Abs(localYear - remoteYear);
|
|
var diff_max = Math.Abs(DateTime.Now.Year - remoteYear);
|
|
dist.AddRatio("year", diff, diff_max);
|
|
}
|
|
|
|
Logger.Trace($"year: {localYear} vs {release.Album.Value.ReleaseDate?.Year} or {release.ReleaseDate?.Year}; {dist.NormalizedDistance()}");
|
|
}
|
|
|
|
// If we parsed a country from the files use that, otherwise use our preference
|
|
var country = localTracks.MostCommon(x => x.FileTrackInfo.Country);
|
|
if (release.Country.Count > 0)
|
|
{
|
|
if (country != null)
|
|
{
|
|
dist.AddEquality("country", country.Name, release.Country);
|
|
Logger.Trace("country: {0} vs {1}; {2}", country.Name, string.Join(", ", release.Country), dist.NormalizedDistance());
|
|
}
|
|
else if (PreferredCountries.Count > 0)
|
|
{
|
|
dist.AddPriority("country", release.Country, PreferredCountries.Select(x => x.Name).ToList());
|
|
Logger.Trace("country priority: {0} vs {1}; {2}", string.Join(", ", PreferredCountries.Select(x => x.Name)), string.Join(", ", release.Country), dist.NormalizedDistance());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// full penalty if MusicBrainz release is missing a country
|
|
dist.Add("country", 1.0);
|
|
}
|
|
|
|
var label = localTracks.MostCommon(x => x.FileTrackInfo.Label);
|
|
if (label.IsNotNullOrWhiteSpace())
|
|
{
|
|
dist.AddEquality("label", label, release.Label);
|
|
Logger.Trace("label: {0} vs {1}; {2}", label, string.Join(", ", release.Label), dist.NormalizedDistance());
|
|
}
|
|
|
|
var disambig = localTracks.MostCommon(x => x.FileTrackInfo.Disambiguation);
|
|
if (disambig.IsNotNullOrWhiteSpace())
|
|
{
|
|
dist.AddString("album_disambiguation", disambig, release.Disambiguation);
|
|
Logger.Trace("album_disambiguation: {0} vs {1}; {2}", disambig, release.Disambiguation, dist.NormalizedDistance());
|
|
}
|
|
|
|
var mbAlbumId = localTracks.MostCommon(x => x.FileTrackInfo.ReleaseMBId);
|
|
if (mbAlbumId.IsNotNullOrWhiteSpace())
|
|
{
|
|
dist.AddBool("album_id", mbAlbumId != release.ForeignReleaseId && !release.OldForeignReleaseIds.Contains(mbAlbumId));
|
|
Logger.Trace("album_id: {0} vs {1} or {2}; {3}", mbAlbumId, release.ForeignReleaseId, string.Join(", ", release.OldForeignReleaseIds), dist.NormalizedDistance());
|
|
}
|
|
|
|
// tracks
|
|
if (mapping.CuesheetTrackMapping.Count != 0)
|
|
{
|
|
foreach (var pair in mapping.CuesheetTrackMapping)
|
|
{
|
|
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
|
}
|
|
|
|
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
|
}
|
|
else
|
|
{
|
|
foreach (var pair in mapping.Mapping)
|
|
{
|
|
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
|
}
|
|
|
|
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
|
|
|
// unmatched tracks
|
|
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
|
|
{
|
|
dist.Add("unmatched_tracks", 1.0);
|
|
}
|
|
|
|
Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
|
|
}
|
|
|
|
// missing tracks
|
|
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
|
{
|
|
dist.Add("missing_tracks", 1.0);
|
|
}
|
|
|
|
Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance());
|
|
|
|
return dist;
|
|
}
|
|
}
|
|
}
|