diff --git a/src/NzbDrone.Core/Datastore/Migration/035_release_group_alias.cs b/src/NzbDrone.Core/Datastore/Migration/035_release_group_alias.cs new file mode 100644 index 000000000..0f9a7e408 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/035_release_group_alias.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(35)] + public class release_group_alias : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Albums").AddColumn("Aliases").AsString().WithDefaultValue("[]"); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs index c2ea90be6..0c9bdd338 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs @@ -30,16 +30,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search { return Decision.Accept(); } - - if (Parser.Parser.CleanArtistName(singleAlbumSpec.AlbumTitle) != Parser.Parser.CleanArtistName(remoteAlbum.ParsedAlbumInfo.AlbumTitle)) + + if (!remoteAlbum.Albums.Any(x => x.Title == singleAlbumSpec.AlbumTitle)) { _logger.Debug("Album does not match searched album title, skipping."); return Decision.Reject("Wrong album"); } - if (!remoteAlbum.ParsedAlbumInfo.AlbumTitle.Any()) + if (remoteAlbum.Albums.Count > 1) { - _logger.Debug("Full discography result during single album search, skipping."); + _logger.Debug("Discography result during single album search, skipping."); return Decision.Reject("Full artist pack"); } diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/AlbumSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/AlbumSearchCriteria.cs index 3c46443f7..f1e44a60f 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/AlbumSearchCriteria.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/AlbumSearchCriteria.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using NzbDrone.Common.Extensions; namespace NzbDrone.Core.IndexerSearch.Definitions @@ -5,10 +8,21 @@ namespace NzbDrone.Core.IndexerSearch.Definitions public class AlbumSearchCriteria : SearchCriteriaBase { public string AlbumTitle { get; set; } + public List AlbumAliases { get; set; } public int AlbumYear { get; set; } public string Disambiguation { get; set; } - public string AlbumQuery => GetQueryTitle($"{AlbumTitle}{(Disambiguation.IsNullOrWhiteSpace() ? string.Empty : $"+{Disambiguation}")}"); + public string AlbumQuery => GetQueryTitle(AddDisambiguation(AlbumTitle)); + + public List AlbumQueries => OrderQueries(AlbumTitle, AlbumAliases) + .Select(x => GetQueryTitle(AddDisambiguation(x))) + .Distinct() + .ToList(); + + private string AddDisambiguation(string term) + { + return Disambiguation.IsNullOrWhiteSpace() ? term : $"{term}+{Disambiguation}"; + } public override string ToString() { diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 57b939fd1..af93b3c58 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions private static readonly Regex SpecialCharacter = new Regex(@"[`'’.]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex IsAllWord = new Regex(@"^[\sA-Za-z0-9_`'’.&:-]*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); public virtual bool MonitoredEpisodesOnly { get; set; } public virtual bool UserInvokedSearch { get; set; } @@ -22,6 +23,39 @@ namespace NzbDrone.Core.IndexerSearch.Definitions public List Tracks { get; set; } public string ArtistQuery => GetQueryTitle(Artist.Name); + public List ArtistQueries => OrderQueries(Artist.Metadata.Value.Name, Artist.Metadata.Value.Aliases); + + protected List OrderQueries(string title, List aliases) + { + var result = new List(); + + // find the primary search term. This will be title if there are no special characters in the title, + // otherwise the first alias with no special characters + if (IsAllWord.IsMatch(title)) + { + result.Add(title); + } + else + { + result.Add(aliases.FirstOrDefault(x => IsAllWord.IsMatch(x)) ?? title); + result.Add(title); + } + + // insert remaining aliases + result.AddRange(aliases.Except(result)); + + return result; + } + + protected List> GetQueryTiers(List titles) + { + var result = new List>(); + + var queries = titles.Select(GetQueryTitle).Distinct(); + result.Add(queries.Take(1).ToList()); + result.Add(queries.Skip(1).ToList()); + return result; + } public static string GetQueryTitle(string title) { diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index c1ce05294..908d731e6 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -73,6 +73,7 @@ namespace NzbDrone.Core.IndexerSearch var searchSpec = Get(artist, new List { album }, userInvokedSearch, interactiveSearch); searchSpec.AlbumTitle = album.Title; + searchSpec.AlbumAliases = album.Aliases; if (album.ReleaseDate.HasValue) { searchSpec.AlbumYear = album.ReleaseDate.Value.Year; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 21f31e46f..25bbbeb94 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; @@ -69,42 +70,83 @@ namespace NzbDrone.Core.Indexers.Newznab if (SupportsAudioSearch) { - AddAudioPageableRequests(pageableRequests, searchCriteria, - NewsnabifyTitle($"&artist={searchCriteria.ArtistQuery}&album={searchCriteria.AlbumQuery}")); + AddAlbumRequests(pageableRequests, searchCriteria, "&artist={0}&album={1}", AddAudioPageableRequests); } if (SupportsSearch) { - pageableRequests.AddTier(); - - pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", - NewsnabifyTitle($"&q={searchCriteria.ArtistQuery}+{searchCriteria.AlbumQuery}"))); + AddAlbumRequests(pageableRequests, searchCriteria, "&q={0}+{1}", AddSearchPageableRequests); } return pageableRequests; } + private void AddAlbumRequests(IndexerPageableRequestChain pageableRequests, AlbumSearchCriteria searchCriteria, string paramFormat, Action AddRequests) + { + var albumQuery = searchCriteria.AlbumQueries[0]; + var artistQuery = searchCriteria.ArtistQueries[0]; + + // search using standard name + pageableRequests.AddTier(); + AddRequests(pageableRequests, searchCriteria, NewsnabifyTitle(string.Format(paramFormat, artistQuery, albumQuery))); + + // using artist alias + pageableRequests.AddTier(); + foreach (var artistAlt in searchCriteria.ArtistQueries.Skip(1)) + { + AddRequests(pageableRequests, searchCriteria, NewsnabifyTitle(string.Format(paramFormat, artistAlt, albumQuery))); + } + + // using album alias + pageableRequests.AddTier(); + foreach (var albumAlt in searchCriteria.AlbumQueries.Skip(1)) + { + AddRequests(pageableRequests, searchCriteria, NewsnabifyTitle(string.Format(paramFormat, artistQuery, albumAlt))); + } + + // using aliases for both + foreach (var artistAlt in searchCriteria.ArtistQueries.Skip(1)) + { + foreach (var albumAlt in searchCriteria.AlbumQueries.Skip(1)) + { + AddRequests(pageableRequests, searchCriteria, NewsnabifyTitle(string.Format(paramFormat, artistAlt, albumAlt))); + } + } + } + public virtual IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); if (SupportsAudioSearch) { - AddAudioPageableRequests(pageableRequests, searchCriteria, - NewsnabifyTitle($"&artist={searchCriteria.ArtistQuery}")); + AddArtistRequests(pageableRequests, searchCriteria, "&artist={0}", AddAudioPageableRequests); } if (SupportsSearch) { - pageableRequests.AddTier(); - - pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", - NewsnabifyTitle($"&q={searchCriteria.ArtistQuery}"))); + AddArtistRequests(pageableRequests, searchCriteria, "&q={0}", AddSearchPageableRequests); } return pageableRequests; } + private void AddArtistRequests(IndexerPageableRequestChain pageableRequests, SearchCriteriaBase searchCriteria, string paramFormat, Action AddRequests) + { + var artistQuery = searchCriteria.ArtistQueries[0]; + + // search using standard name + pageableRequests.AddTier(); + AddRequests(pageableRequests, searchCriteria, NewsnabifyTitle(string.Format(paramFormat, artistQuery))); + + // using artist alias + pageableRequests.AddTier(); + foreach (var artistAlt in searchCriteria.ArtistQueries.Skip(1)) + { + AddRequests(pageableRequests, searchCriteria, NewsnabifyTitle(string.Format(paramFormat, artistAlt))); + } + } + private void AddAudioPageableRequests(IndexerPageableRequestChain chain, SearchCriteriaBase searchCriteria, string parameters) { chain.AddTier(); @@ -112,6 +154,11 @@ namespace NzbDrone.Core.Indexers.Newznab chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "music", $"&q={parameters}")); } + private void AddSearchPageableRequests(IndexerPageableRequestChain chain, SearchCriteriaBase searchCriteria, string parameters) + { + chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", parameters)); + } + private IEnumerable GetPagedRequests(int maxPages, IEnumerable categories, string searchType, string parameters) { if (categories.Empty()) diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs index 16de7ee09..0fb94e25b 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs @@ -5,6 +5,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource { public class AlbumResource { + public AlbumResource() + { + Aliases = new List(); + } + + public List Aliases { get; set; } public string ArtistId { get; set; } public List Artists { get; set; } public string Disambiguation { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index a5af778b9..b6cb4a7bb 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; using NLog; -using NzbDrone.Common.Cloud; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; @@ -300,6 +299,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook album.ForeignAlbumId = resource.Id; album.OldForeignAlbumIds = resource.OldIds; album.Title = resource.Title; + album.Aliases = resource.Aliases; album.Overview = resource.Overview; album.Disambiguation = resource.Disambiguation; album.ReleaseDate = resource.ReleaseDate; diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index 6eec0f258..5d84214cb 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Music { public Album() { + Aliases = new List(); Genres = new List(); Images = new List(); Links = new List(); @@ -28,6 +29,7 @@ namespace NzbDrone.Core.Music public string ForeignAlbumId { get; set; } public List OldForeignAlbumIds { get; set; } public string Title { get; set; } + public List Aliases { get; set; } public string Overview { get; set; } public string Disambiguation { get; set; } public DateTime? ReleaseDate { get; set; } @@ -80,6 +82,7 @@ namespace NzbDrone.Core.Music ForeignAlbumId == other.ForeignAlbumId && (OldForeignAlbumIds?.SequenceEqual(other.OldForeignAlbumIds) ?? true) && Title == other.Title && + (Aliases?.SequenceEqual(other.Aliases) ?? true) && Overview == other.Overview && Disambiguation == other.Disambiguation && ReleaseDate == other.ReleaseDate && @@ -123,6 +126,7 @@ namespace NzbDrone.Core.Music hash = hash * 23 + ForeignAlbumId.GetHashCode(); hash = hash * 23 + OldForeignAlbumIds?.GetHashCode() ?? 0; hash = hash * 23 + Title?.GetHashCode() ?? 0; + hash = hash * 23 + Aliases?.GetHashCode() ?? 0; hash = hash * 23 + Overview?.GetHashCode() ?? 0; hash = hash * 23 + Disambiguation?.GetHashCode() ?? 0; hash = hash * 23 + ReleaseDate?.GetHashCode() ?? 0; diff --git a/src/NzbDrone.Core/Music/AlbumService.cs b/src/NzbDrone.Core/Music/AlbumService.cs index 394bfc1e6..d3fbb0bef 100644 --- a/src/NzbDrone.Core/Music/AlbumService.cs +++ b/src/NzbDrone.Core/Music/AlbumService.cs @@ -87,6 +87,7 @@ namespace NzbDrone.Core.Music 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.Aliases.Any() ? a.Aliases.Select(x => x.CleanArtistName().FuzzyMatch(t)).Max() : 0, cleanTitle), 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()), diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 192bb718c..8a888d4a0 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -104,7 +104,8 @@ namespace NzbDrone.Core.Music Func< Func, string, Tuple, string>> tc = Tuple.Create; var scoringFunctions = new List, string>> { tc((a, t) => a.CleanName.FuzzyMatch(t), cleanTitle), - tc((a, t) => a.Name.FuzzyMatch(t), title), + tc((a, t) => a.Metadata.Value.Name.FuzzyMatch(t), title), + tc((a, t) => a.Metadata.Value.Aliases.Any() ? a.Metadata.Value.Aliases.Select(x => x.CleanArtistName().FuzzyMatch(t)).Max() : 0, cleanTitle) }; if (title.StartsWith("The ", StringComparison.CurrentCultureIgnoreCase)) diff --git a/src/NzbDrone.Core/Music/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/RefreshAlbumService.cs index f850a03ac..5ac67a4df 100644 --- a/src/NzbDrone.Core/Music/RefreshAlbumService.cs +++ b/src/NzbDrone.Core/Music/RefreshAlbumService.cs @@ -163,6 +163,7 @@ namespace NzbDrone.Core.Music local.LastInfoSync = DateTime.UtcNow; local.CleanTitle = remote.CleanTitle; local.Title = remote.Title ?? "Unknown"; + local.Aliases = remote.Aliases; local.Overview = remote.Overview.IsNullOrWhiteSpace() ? local.Overview : remote.Overview; local.Disambiguation = remote.Disambiguation; local.AlbumType = remote.AlbumType; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 8722cd90c..74f2b0b5a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -174,6 +174,7 @@ + @@ -1355,4 +1356,4 @@ --> - \ No newline at end of file + diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index da7476405..9b405551d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -322,8 +322,11 @@ namespace NzbDrone.Core.Parser simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty); - var escapedArtist = Regex.Escape(artist.Name.RemoveAccent()).Replace(@"\ ", @"[\W_]"); - var escapedAlbums = string.Join("|", album.Select(s => Regex.Escape(s.Title.RemoveAccent())).ToList()).Replace(@"\ ", @"[\W_]"); + var artistAliases = new [] { artist.Name }.Concat(artist.Metadata.Value.Aliases); + var escapedArtist = string.Join("|", artistAliases.Select(x => Regex.Escape(x.RemoveAccent())).ToList()).Replace(@"\ ", @"[\W_]"); + + var albumAliases = album.Select(x => x.Title).Concat(album.SelectMany(x => x.Aliases)); + var escapedAlbums = string.Join("|", albumAliases.Select(s => Regex.Escape(s.RemoveAccent())).ToList()).Replace(@"\ ", @"[\W_]"); var releaseRegex = new Regex(@"^(\W*|\b)(?" + escapedArtist + @")(\W*|\b).*(\W*|\b)(?" + escapedAlbums + @")(\W*|\b)", RegexOptions.IgnoreCase);