diff --git a/src/NzbDrone.Common.Test/ExtensionTests/FuzzyContainsFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/FuzzyContainsFixture.cs new file mode 100644 index 000000000..76e87888f --- /dev/null +++ b/src/NzbDrone.Common.Test/ExtensionTests/FuzzyContainsFixture.cs @@ -0,0 +1,61 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Test.Common; + +namespace NzbDrone.Common.Test +{ + [TestFixture] + public class FuzzyContainsFixture : TestBase + { + [TestCase("abcdef", "abcdef", 0.5, 0)] + [TestCase("", "abcdef", 0.5, -1)] + [TestCase("abcdef", "", 0.5, -1)] + [TestCase("", "", 0.5, -1)] + [TestCase("abcdef", "de", 0.5, 3)] + [TestCase("abcdef", "defy", 0.5, 3)] + [TestCase("abcdef", "abcdefy", 0.5, 0)] + [TestCase("I am the very model of a modern major general.", " that berry ", 0.3, 4)] + [TestCase("abcdefghijk", "fgh", 0.5, 5)] + [TestCase("abcdefghijk", "fgh", 0.5, 5)] + [TestCase("abcdefghijk", "efxhi", 0.5, 4)] + [TestCase("abcdefghijk", "cdefxyhijk", 0.5, 2)] + [TestCase("abcdefghijk", "bxy", 0.5, -1)] + [TestCase("123456789xx0", "3456789x0", 0.5, 2)] + [TestCase("abcdef", "xxabc", 0.5, 0)] + [TestCase("abcdef", "defyy", 0.5, 3)] + [TestCase("abcdef", "xabcdefy", 0.5, 0)] + [TestCase("abcdefghijk", "efxyhi", 0.6, 4)] + [TestCase("abcdefghijk", "efxyhi", 0.7, -1)] + [TestCase("abcdefghijk", "bcdef", 0.0, 1)] + [TestCase("abcdexyzabcde", "abccde", 0.5, 0)] + [TestCase("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 0.5, 0)] + [TestCase("abcdefghijklmnopqrstuvwxyz", "abcdefg", 0.5, 0)] + [TestCase("The quick brown fox jumps over the lazy dog", "The quick brown fox jumps over the lazy d", 0.5, 0)] + [TestCase("The quick brown fox jumps over the lazy dog", "The quick brown fox jumps over the lazy g", 0.5, 0)] + [TestCase("The quick brown fox jumps over the lazy dog", "quikc brown fox jumps over the lazy dog", 0.5, 4)] + [TestCase("The quick brown fox jumps over the lazy dog", "qui jumps over the lazy dog", 0.5, 16)] + [TestCase("The quick brown fox jumps over the lazy dog", "quikc brown fox jumps over the lazy dog", 0.5, 4)] + [TestCase("u6IEytQiYpzAccsbjQ5ISuE4smDQ1ZiU42cFBrTeKB2XrVLEqAvgIiKlDP75iApy07jzmK", "xEytQiYpzAccsbjQ5ISuE4smDQ1ZiU42cFBrTeKB2XrVLEqAvgIiKlDP75iApy07jzmK", 0.5, 2)] + [TestCase("plusifeelneedforredundantinformationintitlefield", "anthology", 0.5, -1)] + public void FuzzyFind(string text, string pattern, double threshold, int expected) + { + text.FuzzyFind(pattern, threshold).Should().Be(expected); + } + + [TestCase("abcdef", "abcdef", 1)] + [TestCase("", "abcdef", 0)] + [TestCase("abcdef", "", 0)] + [TestCase("", "", 0)] + [TestCase("abcdef", "de", 1)] + [TestCase("abcdef", "defy", 0.75)] + [TestCase("abcdef", "abcdefghk", 6.0/9)] + [TestCase("abcdef", "zabcdefz", 6.0/8)] + [TestCase("plusifeelneedforredundantinformationintitlefield", "anthology", 4.0/9)] + [TestCase("+ (Plus) - I feel the need for redundant information in the title field", "+", 1)] + public void FuzzyContains(string text, string pattern, double expectedScore) + { + text.FuzzyContains(pattern).Should().BeApproximately(expectedScore, 1e-9); + } + } +} diff --git a/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs b/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs index d8711191b..aff7e9738 100644 --- a/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs +++ b/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs @@ -42,5 +42,21 @@ namespace NzbDrone.Common.Test { text.ToLower().LevenshteinDistanceClean(other.ToLower()).Should().Be(expected); } + + [TestCase("hello", "hello")] + [TestCase("hello", "bye")] + [TestCase("a longer string", "a different long string")] + public void FuzzyMatchSymmetric(string a, string b) + { + a.FuzzyMatch(b).Should().Be(b.FuzzyMatch(a)); + } + + [TestCase("", "", 0)] + [TestCase("a", "", 0)] + [TestCase("", "a", 0)] + public void FuzzyMatchEmptyValuesReturnZero(string a, string b, double expected) + { + a.FuzzyMatch(b).Should().Be(expected); + } } } diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index e49b6b937..69459d7b1 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -84,6 +84,7 @@ + diff --git a/src/NzbDrone.Common/Extensions/FuzzyContains.cs b/src/NzbDrone.Common/Extensions/FuzzyContains.cs new file mode 100644 index 000000000..6a372c44f --- /dev/null +++ b/src/NzbDrone.Common/Extensions/FuzzyContains.cs @@ -0,0 +1,167 @@ +/* + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace NzbDrone.Common.Extensions +{ + + public static class FuzzyContainsExtension { + + public static int FuzzyFind(this string text, string pattern, double matchProb) + { + return match(text, pattern, matchProb).Item1; + } + + // return the accuracy of the best match of pattern within text + public static double FuzzyContains(this string text, string pattern) + { + return match(text, pattern, 0.25).Item2; + } + + /** + * Locate the best instance of 'pattern' in 'text'. + * Returns (-1, 1) if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @return Best match index or -1. + */ + private static Tuple match(string text, string pattern, double matchThreshold = 0.5) { + // Check for null inputs not needed since null can't be passed in C#. + if (text.Length == 0 || pattern.Length == 0) { + // Nothing to match. + return new Tuple (-1, 0); + } + + if (pattern.Length <= text.Length) + { + var loc = text.IndexOf(pattern, StringComparison.Ordinal); + if (loc != -1) + { + // Perfect match! + return new Tuple (loc, 1); + } + } + + // Do a fuzzy compare. + return match_bitap(text, pattern, matchThreshold); + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @return Best match index or -1. + */ + private static Tuple match_bitap(string text, string pattern, double matchThreshold) { + + // Initialise the alphabet. + Dictionary s = alphabet(pattern); + // don't keep creating new BigInteger(1) + var big1 = new BigInteger(1); + + // Lowest score belowe which we give up. + var score_threshold = matchThreshold; + + // Initialise the bit arrays. + var matchmask = big1 << (pattern.Length - 1); + int best_loc = -1; + + // Empty initialization added to appease C# compiler. + var last_rd = new BigInteger[0]; + for (int d = 0; d < pattern.Length; d++) { + // Scan for the best match; each iteration allows for one more error. + int start = 1; + int finish = text.Length + pattern.Length; + + var rd = new BigInteger[finish + 2]; + rd[finish + 1] = (big1 << d) - big1; + for (int j = finish; j >= start; j--) { + BigInteger charMatch; + if (text.Length <= j - 1 || !s.ContainsKey(text[j - 1])) { + // Out of range. + charMatch = 0; + } else { + charMatch = s[text[j - 1]]; + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | big1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | big1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | big1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + var score = bitapScore(d, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score >= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + } + } + } + if (bitapScore(d + 1, pattern) < score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return new Tuple (best_loc, score_threshold); + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param pattern Pattern being sought. + * @return Overall score for match (1.0 = good, 0.0 = bad). + */ + private static double bitapScore(int e, string pattern) { + return 1.0 - (double)e / pattern.Length; + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + private static Dictionary alphabet(string pattern) { + var s = new Dictionary(); + char[] char_pattern = pattern.ToCharArray(); + foreach (char c in char_pattern) { + if (!s.ContainsKey(c)) { + s.Add(c, 0); + } + } + int i = 0; + foreach (char c in char_pattern) { + s[c] = s[c] | (new BigInteger(1) << (pattern.Length - i - 1)); + i++; + } + return s; + } + } +} diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 40c7886f6..1fe5b90f2 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -143,29 +143,17 @@ namespace NzbDrone.Common.Extensions public static double FuzzyMatch(this string a, string b) { - if (a.Contains(" ") && b.Contains(" ")) + if (a.IsNullOrWhiteSpace() || b.IsNullOrWhiteSpace()) + { + return 0; + } + else if (a.Contains(" ") && b.Contains(" ")) { var partsA = a.Split(' '); var partsB = b.Split(' '); - var weightedHighCoefficients = new double[partsA.Length]; - var distanceRatios = new double[partsA.Length]; - for (int i = 0; i < partsA.Length; i++) - { - double high = 0.0; - int indexDistance = 0; - for (int x = 0; x < partsB.Length; x++) - { - var coef = LevenshteinCoefficient(partsA[i], partsB[x]); - if (coef > high) - { - high = coef; - indexDistance = Math.Abs(i - x); - } - } - double distanceWeight = 1.0 - (double)indexDistance / (double)partsA.Length; - weightedHighCoefficients[i] = high * distanceWeight; - } - return weightedHighCoefficients.Sum() / (double)partsA.Length; + + var coef = (FuzzyMatchComponents(partsA, partsB) + FuzzyMatchComponents(partsB, partsA)) / (partsA.Length + partsB.Length); + return Math.Max(coef, LevenshteinCoefficient(a, b)); } else { @@ -173,6 +161,28 @@ namespace NzbDrone.Common.Extensions } } + private static double FuzzyMatchComponents(string[] a, string[] b) + { + double weightDenom = Math.Max(a.Length, b.Length); + double sum = 0; + for (int i = 0; i < a.Length; i++) + { + double high = 0.0; + int indexDistance = 0; + for (int x = 0; x < b.Length; x++) + { + var coef = LevenshteinCoefficient(a[i], b[x]); + if (coef > high) + { + high = coef; + indexDistance = Math.Abs(i - x); + } + } + sum += (1.0 - (double)indexDistance / weightDenom) * high; + } + return sum; + } + public static double LevenshteinCoefficient(this string a, string b) { return 1.0 - (double)a.LevenshteinDistance(b) / Math.Max(a.Length, b.Length); diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 08fd5c764..bd08eacb1 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -71,6 +71,7 @@ + @@ -198,6 +199,7 @@ + diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs index d924bd3ab..d7245756d 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs @@ -94,7 +94,6 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests } - [Test] public void should_find_album_in_db_by_releaseid() { @@ -129,6 +128,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests [TestCase("ANTholog")] [TestCase("nthology")] [TestCase("antholoyg")] + [TestCase("÷")] public void should_not_find_album_in_db_by_incorrect_title(string title) { var album = _albumRepo.FindByTitle(_artist.Id, title); @@ -136,28 +136,6 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests album.Should().BeNull(); } - [TestCase("ANTholog")] - [TestCase("antholoyg")] - [TestCase("ANThology CD")] - public void should_find_album_in_db_by_inexact_title(string title) - { - var album = _albumRepo.FindByTitleInexact(_artist.Id, title); - - album.Should().NotBeNull(); - album.Title.Should().Be(_album.Title); - } - - [TestCase("ANTholog")] - [TestCase("antholoyg")] - [TestCase("ANThology CD")] - public void should_not_find_album_in_db_by_inexact_title_when_two_similar_matches(string title) - { - _albumRepo.Insert(_albumSimilar); - var album = _albumRepo.FindByTitleInexact(_artist.Id, title); - - album.Should().BeNull(); - } - [Test] public void should_not_find_album_in_db_by_partial_releaseid() { diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs new file mode 100644 index 000000000..1a44e97ec --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs @@ -0,0 +1,77 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Core.Test.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using Moq; + +namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests +{ + [TestFixture] + public class AlbumServiceFixture : CoreTest + { + private List _albums; + + [SetUp] + public void Setup() + { + _albums = new List(); + _albums.Add(new Album + { + Title = "ANThology", + CleanTitle = "anthology", + }); + + _albums.Add(new Album + { + Title = "+", + CleanTitle = "", + }); + + Mocker.GetMock() + .Setup(s => s.GetAlbums(It.IsAny())) + .Returns(_albums); + } + + private void GivenSimilarAlbum() + { + _albums.Add(new Album + { + Title = "ANThology2", + CleanTitle = "anthology2", + }); + } + + [TestCase("ANTholog", "ANThology")] + [TestCase("antholoyg", "ANThology")] + [TestCase("ANThology CD", "ANThology")] + [TestCase("ANThology CD xxxx (Remastered) - [Oh please why do they do this?]", "ANThology")] + [TestCase("+ (Plus) - I feel the need for redundant information in the title field", "+")] + public void should_find_album_in_db_by_inexact_title(string title, string expected) + { + var album = Subject.FindByTitleInexact(0, title); + + album.Should().NotBeNull(); + album.Title.Should().Be(expected); + } + + [TestCase("ANTholog")] + [TestCase("antholoyg")] + [TestCase("ANThology CD")] + [TestCase("÷")] + [TestCase("÷ (Divide)")] + public void should_not_find_album_in_db_by_inexact_title_when_two_similar_matches(string title) + { + GivenSimilarAlbum(); + var album = Subject.FindByTitleInexact(0, title); + + album.Should().BeNull(); + } + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs index 814a20ab0..0142842a3 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs @@ -17,6 +17,24 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests public class ArtistRepositoryFixture : DbTest { + private ArtistRepository _artistRepo; + + private Artist CreateArtist(string name) + { + return Builder.CreateNew() + .With(a => a.Name = name) + .With(a => a.CleanName = Parser.Parser.CleanArtistName(name)) + .With(a => a.ForeignArtistId = name) + .BuildNew(); + } + + private void GivenArtists() + { + _artistRepo = Mocker.Resolve(); + _artistRepo.Insert(CreateArtist("The Black Eyed Peas")); + _artistRepo.Insert(CreateArtist("The Black Keys")); + } + [Test] public void should_lazyload_profiles() { @@ -61,5 +79,16 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests StoredModel.MetadataProfile.Should().NotBeNull(); } + + [TestCase("The Black Eyed Peas")] + [TestCase("The Black Keys")] + public void should_find_artist_in_db_by_name(string name) + { + GivenArtists(); + var artist = _artistRepo.FindByName(Parser.Parser.CleanArtistName(name)); + + artist.Should().NotBeNull(); + artist.Name.Should().Be(name); + } } } diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs new file mode 100644 index 000000000..2c11b65d4 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests +{ + [TestFixture] + + public class FindByNameInexactFixture : CoreTest + { + private List _artists; + + private Artist CreateArtist(string name) + { + return Builder.CreateNew() + .With(a => a.Name = name) + .With(a => a.CleanName = Parser.Parser.CleanArtistName(name)) + .With(a => a.ForeignArtistId = name) + .BuildNew(); + } + + [SetUp] + public void Setup() + { + _artists = new List(); + _artists.Add(CreateArtist("The Black Eyed Peas")); + _artists.Add(CreateArtist("The Black Keys")); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(_artists); + } + + [TestCase("The Black Eyde Peas", "The Black Eyed Peas")] + [TestCase("Black Eyed Peas", "The Black Eyed Peas")] + [TestCase("The Black eys", "The Black Keys")] + public void should_find_artist_in_db_by_name_inexact(string name, string expected) + { + var artist = Subject.FindByNameInexact(name); + + artist.Should().NotBeNull(); + artist.Name.Should().Be(expected); + } + + [TestCase("The Black Peas")] + public void should_not_find_artist_in_db_by_ambiguous_name(string name) + { + + var artist = Subject.FindByNameInexact(name); + + artist.Should().BeNull(); + } + + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/TitleMatchingTests/TitleMatchingFixture.cs b/src/NzbDrone.Core.Test/MusicTests/TitleMatchingTests/TitleMatchingFixture.cs index f9d10b1da..0594fdcf2 100644 --- a/src/NzbDrone.Core.Test/MusicTests/TitleMatchingTests/TitleMatchingFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/TitleMatchingTests/TitleMatchingFixture.cs @@ -1,27 +1,21 @@ using System.Linq; using FluentAssertions; -using NLog; using NUnit.Framework; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; using System.Collections.Generic; +using Moq; namespace NzbDrone.Core.Test.MusicTests.TitleMatchingTests { [TestFixture] - public class TitleMatchingFixture : DbTest + public class TitleMatchingFixture : CoreTest { - private TrackRepository _trackRepository; - private TrackService _trackService; - + private List _tracks; + [SetUp] public void Setup() { - _trackRepository = Mocker.Resolve(); - _trackService = - new TrackService(_trackRepository, Mocker.Resolve(), Mocker.Resolve()); - var trackNames = new List { "Courage", "Movies", @@ -35,45 +29,94 @@ namespace NzbDrone.Core.Test.MusicTests.TitleMatchingTests "Calico", "(Happy) Death Day", "Smooth Criminal", - "Universe / Orange Appeal" + "Universe / Orange Appeal", + "Christian's Inferno" + }; + + _tracks = new List(); + for (int i = 0; i < trackNames.Count; i++) { + _tracks.Add(new Track + { + Title = trackNames[i], + ForeignTrackId = (i+1).ToString(), + AbsoluteTrackNumber = i+1, + MediumNumber = 1 + }); + } + + Mocker.GetMock() + .Setup(s => s.GetTracksByMedium(It.IsAny(), It.IsAny())) + .Returns(_tracks); + + Mocker.GetMock() + .Setup(s => s.Find(1234, 4321, It.IsAny(), It.IsAny())) + .Returns((int artistid, int albumid, int medium, int track) => _tracks.Where(t => t.AbsoluteTrackNumber == track && t.MediumNumber == medium).Single()); + } + + private void GivenSecondDisc() + { + var trackNames = new List { + "Courage", + "another entry", + "random name" }; for (int i = 0; i < trackNames.Count; i++) { - _trackRepository.Insert(new Track - { - Title = trackNames[i], - ForeignTrackId = (i+1).ToString(), - AlbumId = 4321, - AbsoluteTrackNumber = i+1, - MediumNumber = 1, - TrackFileId = i+1 - }); + _tracks.Add(new Track + { + Title = trackNames[i], + ForeignTrackId = (100+i+1).ToString(), + AbsoluteTrackNumber = i+1, + MediumNumber = 2 + }); } } [Test] public void should_find_track_in_db_by_tracktitle_longer_then_releasetitle() { - var track = _trackService.FindTrackByTitle(1234, 4321, 1, 1, "Courage with some bla"); + var track = Subject.FindTrackByTitle(1234, 4321, 1, 1, "Courage with some bla"); track.Should().NotBeNull(); - track.Title.Should().Be(_trackRepository.GetTracksByFileId(1).First().Title); + track.Title.Should().Be(Subject.FindTrack(1234, 4321, 1, 1).Title); } [Test] public void should_find_track_in_db_by_tracktitle_shorter_then_releasetitle() { - var track = _trackService.FindTrackByTitle(1234, 4321, 1, 3, "and Bone"); + var track = Subject.FindTrackByTitle(1234, 4321, 1, 3, "and Bone"); track.Should().NotBeNull(); - track.Title.Should().Be(_trackRepository.GetTracksByFileId(3).First().Title); + track.Title.Should().Be(Subject.FindTrack(1234, 4321, 1, 3).Title); } [Test] public void should_not_find_track_in_db_by_wrong_title() { - var track = _trackService.FindTrackByTitle(1234, 4321, 1, 1, "Not a track"); + var track = Subject.FindTrackByTitle(1234, 4321, 1, 1, "Not a track"); + + track.Should().BeNull(); + } + + [TestCase("another entry", 2, 2)] + [TestCase("random name", 2, 3)] + public void should_find_track_on_second_disc_when_disc_tag_missing(string title, int discNumber, int trackNumber) + { + GivenSecondDisc(); + var track = Subject.FindTrackByTitle(1234, 4321, 0, trackNumber, title); + var expected = Subject.FindTrack(1234, 4321, discNumber, trackNumber); + track.Should().NotBeNull(); + expected.Should().NotBeNull(); + + track.Title.Should().Be(expected.Title); + } + + [Test] + public void should_return_null_if_tracks_with_same_name_and_number_on_different_discs() + { + GivenSecondDisc(); + var track = Subject.FindTrackByTitle(1234, 4321, 0, 1, "Courage"); track.Should().BeNull(); } @@ -81,19 +124,53 @@ namespace NzbDrone.Core.Test.MusicTests.TitleMatchingTests [TestCase("Atitude", 7)] [TestCase("Smoth cRimnal", 12)] [TestCase("Sticks and Stones (live)", 6)] + [TestCase("Sticks and Stones (live) - there's a lot of rubbish here", 6)] + [TestCase("Smoth cRimnal feat. someone I don't care about", 12)] + [TestCase("Christians Inferno", 14)] + [TestCase("xxxyyy some random prefix Christians Infurno", 14)] public void should_find_track_in_db_by_inexact_title(string title, int trackNumber) { - var track = _trackService.FindTrackByTitleInexact(1234, 4321, 1, trackNumber, title); + var track = Subject.FindTrackByTitleInexact(1234, 4321, 1, trackNumber, title); + var expected = Subject.FindTrack(1234, 4321, 1, trackNumber); + + track.Should().NotBeNull(); + expected.Should().NotBeNull(); + + track.Title.Should().Be(expected.Title); + } + + [TestCase("Fesh and Bone", 1)] + [TestCase("Atitude", 1)] + [TestCase("Smoth cRimnal", 1)] + [TestCase("Sticks and Stones (live)", 1)] + [TestCase("Christians Inferno", 1)] + public void should_not_find_track_in_db_by_inexact_title_with_wrong_tracknumber(string title, int trackNumber) + { + var track = Subject.FindTrackByTitleInexact(1234, 4321, 1, trackNumber, title); + + track.Should().BeNull(); + } + + [TestCase("Movis", 1, 2)] + [TestCase("anoth entry", 2, 2)] + [TestCase("random.name", 2, 3)] + public void should_find_track_in_db_by_inexact_title_when_disc_tag_missing(string title, int discNumber, int trackNumber) + { + GivenSecondDisc(); + var track = Subject.FindTrackByTitleInexact(1234, 4321, 0, trackNumber, title); + var expected = Subject.FindTrack(1234, 4321, discNumber, trackNumber); track.Should().NotBeNull(); - track.Title.Should().Be(_trackRepository.GetTracksByFileId(trackNumber).First().Title); + expected.Should().NotBeNull(); + + track.Title.Should().Be(expected.Title); } [TestCase("A random title", 1)] [TestCase("Stones and Sticks", 6)] public void should_not_find_track_in_db_by_different_inexact_title(string title, int trackId) { - var track = _trackService.FindTrackByTitleInexact(1234, 4321, 1, trackId, title); + var track = Subject.FindTrackByTitleInexact(1234, 4321, 1, trackId, title); track.Should().BeNull(); } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 0d5389469..ccd655a00 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -299,12 +299,14 @@ + + diff --git a/src/NzbDrone.Core/Music/AlbumRepository.cs b/src/NzbDrone.Core/Music/AlbumRepository.cs index db2144da1..ff4162280 100644 --- a/src/NzbDrone.Core/Music/AlbumRepository.cs +++ b/src/NzbDrone.Core/Music/AlbumRepository.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; -using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Music { @@ -17,7 +16,6 @@ namespace NzbDrone.Core.Music List GetAlbums(int artistId); Album FindByName(string cleanTitle); Album FindByTitle(int artistId, string title); - Album FindByTitleInexact(int artistId, string title); Album FindByArtistAndName(string artistName, string cleanTitle); Album FindById(string spotifyId); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); @@ -49,7 +47,7 @@ namespace NzbDrone.Core.Music public Album FindById(string foreignAlbumId) { - return Query.SingleOrDefault(s => s.ForeignAlbumId == foreignAlbumId); + return Query.Where(s => s.ForeignAlbumId == foreignAlbumId).SingleOrDefault(); } public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) @@ -285,7 +283,7 @@ namespace NzbDrone.Core.Music { cleanTitle = cleanTitle.ToLowerInvariant(); - return Query.SingleOrDefault(s => s.CleanTitle == cleanTitle); + return Query.Where(s => s.CleanTitle == cleanTitle).SingleOrDefault(); } public Album FindByTitle(int artistId, string title) @@ -300,39 +298,6 @@ namespace NzbDrone.Core.Music .FirstOrDefault(); } - public Album FindByTitleInexact(int artistId, string title) - { - double fuzzThreshold = 0.7; - double fuzzGap = 0.4; - var cleanTitle = Parser.Parser.CleanArtistName(title); - - if (string.IsNullOrEmpty(cleanTitle)) - cleanTitle = title; - - var sortedAlbums = Query.Where(s => s.ArtistId == artistId) - .Select(s => new - { - MatchProb = s.CleanTitle.FuzzyMatch(cleanTitle), - Album = s - }) - .ToList() - .OrderByDescending(s => s.MatchProb) - .ToList(); - - if (!sortedAlbums.Any()) - return null; - - _logger.Trace("\nFuzzy album match on '{0}':\n{1}", - cleanTitle, - string.Join("\n", sortedAlbums.Select(x => $"{x.Album.CleanTitle}: {x.MatchProb}"))); - - if (sortedAlbums[0].MatchProb > fuzzThreshold - && (sortedAlbums.Count == 1 || sortedAlbums[0].MatchProb - sortedAlbums[1].MatchProb > fuzzGap)) - return sortedAlbums[0].Album; - - return null; - } - public Album FindByArtistAndName(string artistName, string cleanTitle) { var cleanArtistName = Parser.Parser.CleanArtistName(artistName); @@ -340,7 +305,8 @@ namespace NzbDrone.Core.Music return Query.Join(JoinType.Inner, album => album.Artist, (album, artist) => album.ArtistId == artist.Id) .Where(artist => artist.CleanName == cleanArtistName) - .SingleOrDefault(album => album.CleanTitle == cleanTitle); + .Where(album => album.CleanTitle == cleanTitle) + .SingleOrDefault(); } public Album FindAlbumByRelease(string releaseId) diff --git a/src/NzbDrone.Core/Music/AlbumService.cs b/src/NzbDrone.Core/Music/AlbumService.cs index e5285c1bc..29a8d8ad2 100644 --- a/src/NzbDrone.Core/Music/AlbumService.cs +++ b/src/NzbDrone.Core/Music/AlbumService.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Parser; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Music { @@ -89,7 +91,63 @@ namespace NzbDrone.Core.Music public Album FindByTitleInexact(int artistId, string title) { - return _albumRepository.FindByTitleInexact(artistId, title); + var cleanTitle = title.CleanArtistName(); + + var albums = GetAlbumsByArtist(artistId); + + Func< Func, string, Tuple, string>> tc = Tuple.Create; + var scoringFunctions = new List, string>> { + tc((a, t) => a.CleanTitle.FuzzyMatch(t), cleanTitle), + tc((a, t) => a.Title.FuzzyMatch(t), title), + tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveBracketsAndContents().CleanArtistName()), + tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveAfterDash().CleanArtistName()), + tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveBracketsAndContents().RemoveAfterDash().CleanArtistName()), + tc((a, t) => t.FuzzyContains(a.CleanTitle), cleanTitle), + tc((a, t) => t.FuzzyContains(a.Title), title) + }; + + foreach (var func in scoringFunctions) + { + var album = FindByStringInexact(albums, func.Item1, func.Item2); + if (album != null) + { + return album; + } + } + + return null; + } + + private Album FindByStringInexact(List albums, Func scoreFunction, string title) + { + const double fuzzThreshold = 0.7; + const double fuzzGap = 0.4; + + var sortedAlbums = albums.Select(s => new + { + MatchProb = scoreFunction(s, title), + Album = s + }) + .ToList() + .OrderByDescending(s => s.MatchProb) + .ToList(); + + if (!sortedAlbums.Any()) + { + return null; + } + + _logger.Trace("\nFuzzy album match on '{0}':\n{1}", + title, + string.Join("\n", sortedAlbums.Select(x => $"[{x.Album.Title}] {x.Album.CleanTitle}: {x.MatchProb}"))); + + if (sortedAlbums[0].MatchProb > fuzzThreshold + && (sortedAlbums.Count == 1 || sortedAlbums[0].MatchProb - sortedAlbums[1].MatchProb > fuzzGap)) + { + return sortedAlbums[0].Album; + } + + return null; } public List GetAllAlbums() diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 575984422..cba567d10 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Music List AddArtists(List newArtists); Artist FindById(string spotifyId); Artist FindByName(string title); - Artist FindByTitleInexact(string title); + Artist FindByNameInexact(string title); void DeleteArtist(int artistId, bool deleteFiles); List GetAllArtists(); List AllForTag(int tagId); @@ -89,9 +89,43 @@ namespace NzbDrone.Core.Music return _artistRepository.FindByName(title.CleanArtistName()); } - public Artist FindByTitleInexact(string title) + public Artist FindByNameInexact(string title) { - throw new NotImplementedException(); + const double fuzzThreshold = 0.8; + const double fuzzGap = 0.2; + var cleanTitle = Parser.Parser.CleanArtistName(title); + + if (string.IsNullOrEmpty(cleanTitle)) + { + cleanTitle = title; + } + + var sortedArtists = GetAllArtists() + .Select(s => new + { + MatchProb = s.CleanName.FuzzyMatch(cleanTitle), + Artist = s + }) + .ToList() + .OrderByDescending(s => s.MatchProb) + .ToList(); + + if (!sortedArtists.Any()) + { + return null; + } + + _logger.Trace("\nFuzzy artist match on '{0}':\n{1}", + cleanTitle, + string.Join("\n", sortedArtists.Select(x => $"{x.Artist.CleanName}: {x.MatchProb}"))); + + if (sortedArtists[0].MatchProb > fuzzThreshold + && (sortedArtists.Count == 1 || sortedArtists[0].MatchProb - sortedArtists[1].MatchProb > fuzzGap)) + { + return sortedArtists[0].Artist; + } + + return null; } public List GetAllArtists() @@ -110,7 +144,6 @@ namespace NzbDrone.Core.Music return _artistRepository.Get(artistDBId); } - public List GetArtists(IEnumerable artistIds) { return _artistRepository.Get(artistIds).ToList(); diff --git a/src/NzbDrone.Core/Music/TrackRepository.cs b/src/NzbDrone.Core/Music/TrackRepository.cs index 421c9ce60..a4563fc3c 100644 --- a/src/NzbDrone.Core/Music/TrackRepository.cs +++ b/src/NzbDrone.Core/Music/TrackRepository.cs @@ -61,6 +61,11 @@ namespace NzbDrone.Core.Music public List GetTracksByMedium(int albumId, int mediumNumber) { + if (mediumNumber < 1) + { + return GetTracksByAlbum(albumId); + } + return Query.Where(s => s.AlbumId == albumId) .AndWhere(s => s.MediumNumber == mediumNumber) .ToList(); diff --git a/src/NzbDrone.Core/Music/TrackService.cs b/src/NzbDrone.Core/Music/TrackService.cs index df8f8c813..3a47e4672 100644 --- a/src/NzbDrone.Core/Music/TrackService.cs +++ b/src/NzbDrone.Core/Music/TrackService.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Parser; using NzbDrone.Common.Extensions; using System; using System.Collections.Generic; @@ -81,60 +82,73 @@ namespace NzbDrone.Core.Music public Track FindTrackByTitle(int artistId, int albumId, int mediumNumber, int trackNumber, string releaseTitle) { // TODO: can replace this search mechanism with something smarter/faster/better - var normalizedReleaseTitle = Parser.Parser.NormalizeTrackTitle(releaseTitle).Replace(".", " "); + var normalizedReleaseTitle = releaseTitle.NormalizeTrackTitle().Replace(".", " "); var tracks = _trackRepository.GetTracksByMedium(albumId, mediumNumber); - var matches = from track in tracks - //if we have a trackNumber use it - let trackNumCheck = (trackNumber == 0 || track.AbsoluteTrackNumber == trackNumber) - //if release title is longer than track title - let posReleaseTitle = normalizedReleaseTitle.IndexOf(Parser.Parser.NormalizeTrackTitle(track.Title), StringComparison.CurrentCultureIgnoreCase) - //if track title is longer than release title - let posTrackTitle = Parser.Parser.NormalizeTrackTitle(track.Title).IndexOf(normalizedReleaseTitle, StringComparison.CurrentCultureIgnoreCase) - where track.Title.Length > 0 && trackNumCheck && (posReleaseTitle >= 0 || posTrackTitle >= 0) - orderby posReleaseTitle, posTrackTitle - select new - { - NormalizedLength = Parser.Parser.NormalizeTrackTitle(track.Title).Length, - Track = track - }; + var matches = tracks.Where(t => (trackNumber == 0 || t.AbsoluteTrackNumber == trackNumber) + && t.Title.Length > 0 + && (normalizedReleaseTitle.Contains(t.Title.NormalizeTrackTitle()) + || t.Title.NormalizeTrackTitle().Contains(normalizedReleaseTitle))); - return matches.OrderByDescending(e => e.NormalizedLength).FirstOrDefault()?.Track; + return matches.Count() > 1 ? null : matches.SingleOrDefault(); } - public Track FindTrackByTitleInexact(int artistId, int albumId, int mediumNumber, int trackNumber, string releaseTitle) + public Track FindTrackByTitleInexact(int artistId, int albumId, int mediumNumber, int trackNumber, string title) { - double fuzzThreshold = 0.6; - double fuzzGap = 0.2; - - var normalizedReleaseTitle = Parser.Parser.NormalizeTrackTitle(releaseTitle).Replace(".", " "); + var normalizedTitle = title.NormalizeTrackTitle().Replace(".", " "); var tracks = _trackRepository.GetTracksByMedium(albumId, mediumNumber); - var matches = from track in tracks - let normalizedTitle = Parser.Parser.NormalizeTrackTitle(track.Title).Replace(".", " ") - let matchProb = normalizedTitle.FuzzyMatch(normalizedReleaseTitle) - where track.Title.Length > 0 - orderby matchProb descending - select new + Func< Func, string, Tuple, string>> tc = Tuple.Create; + var scoringFunctions = new List, string>> { + tc((a, t) => a.Title.NormalizeTrackTitle().FuzzyMatch(t), normalizedTitle), + tc((a, t) => a.Title.NormalizeTrackTitle().FuzzyContains(t), normalizedTitle), + tc((a, t) => t.FuzzyContains(a.Title.NormalizeTrackTitle()), normalizedTitle) + }; + + foreach (var func in scoringFunctions) + { + var track = FindByStringInexact(tracks, func.Item1, func.Item2, trackNumber); + if (track != null) { - MatchProb = matchProb, - NormalizedTitle = normalizedTitle, - Track = track - }; + return track; + } + } - var matchList = matches.ToList(); + return null; + } - if (!matchList.Any()) + private Track FindByStringInexact(List tracks, Func scoreFunction, string title, int trackNumber) + { + const double fuzzThreshold = 0.7; + const double fuzzGap = 0.2; + + var sortedTracks = tracks.Select(s => new + { + MatchProb = scoreFunction(s, title), + Track = s + }) + .ToList() + .OrderByDescending(s => s.MatchProb) + .ToList(); + + if (!sortedTracks.Any()) + { return null; + } - _logger.Trace("\nFuzzy track match on '{0}':\n{1}", - normalizedReleaseTitle, - string.Join("\n", matchList.Select(x => $"{x.NormalizedTitle}: {x.MatchProb}"))); + _logger.Trace("\nFuzzy track match on '{0:D2} - {1}':\n{2}", + trackNumber, + title, + string.Join("\n", sortedTracks.Select(x => $"{x.Track.AbsoluteTrackNumber:D2} - {x.Track.Title}: {x.MatchProb}"))); - if (matchList[0].MatchProb > fuzzThreshold - && (matchList.Count == 1 || matchList[0].MatchProb - matchList[1].MatchProb > fuzzGap) - && (trackNumber == 0 || matchList[0].Track.AbsoluteTrackNumber == trackNumber)) - return matchList[0].Track; + if (sortedTracks[0].MatchProb > fuzzThreshold + && (sortedTracks.Count == 1 || sortedTracks[0].MatchProb - sortedTracks[1].MatchProb > fuzzGap) + && (trackNumber == 0 + || sortedTracks[0].Track.AbsoluteTrackNumber == trackNumber + || sortedTracks[0].Track.AbsoluteTrackNumber + tracks.Count(t => t.MediumNumber < sortedTracks[0].Track.MediumNumber) == trackNumber)) + { + return sortedTracks[0].Track; + } return null; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 632611e91..49183607b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -208,6 +208,14 @@ namespace NzbDrone.Core.Parser new Regex(@"(\[|\()*\b((featuring|feat.|feat|ft|ft.)\s{1}){1}\s*.*(\]|\))*", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex(@"(?:\(|\[)(?:[^\(\[]*)(?:version|limited|deluxe|single|clean|album|special|bonus|promo|remastered)(?:[^\)\]]*)(?:\)|\])", RegexOptions.IgnoreCase | RegexOptions.Compiled) }; + + private static readonly Regex[] BracketRegex = new Regex[] + { + new Regex(@"\(.*\)", RegexOptions.Compiled), + new Regex(@"\[.*\]", RegexOptions.Compiled) + }; + + private static readonly Regex AfterDashRegex = new Regex(@"[-:].*", RegexOptions.Compiled); public static ParsedTrackInfo ParseMusicPath(string path) { @@ -528,14 +536,13 @@ namespace NzbDrone.Core.Parser return NormalizeRegex.Replace(name, string.Empty).ToLower().RemoveAccent(); } - public static string NormalizeTrackTitle(string title) + public static string NormalizeTrackTitle(this string title) { title = SpecialEpisodeWordRegex.Replace(title, string.Empty); title = PunctuationRegex.Replace(title, " "); title = DuplicateSpacesRegex.Replace(title, " "); - return title.Trim() - .ToLower(); + return title.Trim().ToLower(); } public static string NormalizeTitle(string title) @@ -601,6 +608,22 @@ namespace NzbDrone.Core.Parser return CommonTagRegex[1].Replace(album, string.Empty).Trim(); } + public static string RemoveBracketsAndContents(this string album) + { + var intermediate = album; + foreach (var regex in BracketRegex) + { + intermediate = regex.Replace(intermediate, string.Empty).Trim(); + } + + return intermediate; + } + + public static string RemoveAfterDash(this string text) + { + return AfterDashRegex.Replace(text, string.Empty).Trim(); + } + public static string CleanTrackTitle(string title) { var intermediateTitle = title; @@ -619,7 +642,7 @@ namespace NzbDrone.Core.Parser var trackNumber = file.Tag.Track; var trackTitle = file.Tag.Title; - var discNumber = (file.Tag.Disc > 0) ? Convert.ToInt32(file.Tag.Disc) : 1; + var discNumber = (int)file.Tag.Disc; var artist = file.Tag.FirstAlbumArtist; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 67fb7832d..9aa57c727 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -50,14 +50,21 @@ namespace NzbDrone.Core.Parser public Artist GetArtist(string title) { var parsedAlbumInfo = Parser.ParseAlbumTitle(title); + + if (parsedAlbumInfo != null && !parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace()) + { + title = parsedAlbumInfo.ArtistName; + } - if (parsedAlbumInfo == null || parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace()) + var artistInfo = _artistService.FindByName(title); + + if (artistInfo == null) { - return _artistService.FindByName(title); + _logger.Debug("Trying inexact artist match for {0}", title); + artistInfo = _artistService.FindByNameInexact(title); } - return _artistService.FindByName(parsedAlbumInfo.ArtistName); - + return artistInfo; } public Artist GetArtistFromTag(string file) @@ -81,8 +88,15 @@ namespace NzbDrone.Core.Parser return null; } - return _artistService.FindByName(parsedTrackInfo.ArtistTitle); + artist = _artistService.FindByName(parsedTrackInfo.ArtistTitle); + + if (artist == null) + { + _logger.Debug("Trying inexact artist match for {0}", parsedTrackInfo.ArtistTitle); + artist = _artistService.FindByNameInexact(parsedTrackInfo.ArtistTitle); + } + return artist; } public RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria = null) @@ -147,6 +161,12 @@ namespace NzbDrone.Core.Parser albumInfo = _albumService.FindByTitle(artist.Id, parsedAlbumInfo.AlbumTitle); } + if (albumInfo == null) + { + _logger.Debug("Trying inexact album match for {0}", parsedAlbumInfo.AlbumTitle); + albumInfo = _albumService.FindByTitleInexact(artist.Id, parsedAlbumInfo.AlbumTitle); + } + if (albumInfo != null) { result.Add(albumInfo); @@ -186,6 +206,12 @@ namespace NzbDrone.Core.Parser artist = _artistService.FindByName(parsedAlbumInfo.ArtistName); + if (artist == null) + { + _logger.Debug("Trying inexact artist match for {0}", parsedAlbumInfo.ArtistName); + artist = _artistService.FindByNameInexact(parsedAlbumInfo.ArtistName); + } + if (artist == null) { _logger.Debug("No matching artist {0}", parsedAlbumInfo.ArtistName);