Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,125 @@ public void should_fallback_to_book_title_when_selected_edition_title_is_blank()
Assert.That(title, Is.EqualTo("Alanna: The First Adventure"));
}

[Test]
public void should_use_book_title_when_selected_edition_is_an_omnibus_containing_it()
{
var omnibus = new Edition { Title = "A Game of Thrones / A Clash of Kings" };

var book = new Book
{
Title = "A Clash of Kings",
Editions = new List<Edition> { omnibus }
};

var title = ReleaseSearchService.GetSearchBookTitle(book, omnibus);

Assert.That(title, Is.EqualTo("A Clash of Kings"));
}

[Test]
public void should_use_book_title_when_omnibus_separator_has_no_surrounding_spaces()
{
var omnibus = new Edition { Title = "A Game of Thrones/A Clash of Kings" };

var book = new Book
{
Title = "A Clash of Kings",
Editions = new List<Edition> { omnibus }
};

var title = ReleaseSearchService.GetSearchBookTitle(book, omnibus);

Assert.That(title, Is.EqualTo("A Clash of Kings"));
}

[Test]
public void should_match_omnibus_segment_ignoring_case_and_padding()
{
var omnibus = new Edition { Title = "A GAME OF THRONES / a clash of kings" };

var book = new Book
{
Title = "A Clash of Kings",
Editions = new List<Edition> { omnibus }
};

var title = ReleaseSearchService.GetSearchBookTitle(book, omnibus);

Assert.That(title, Is.EqualTo("A Clash of Kings"));
}

[Test]
public void should_keep_edition_title_when_slash_is_part_of_a_phrase()
{
// "Horror/Sci-Fi" is one phrase, not two works. Splitting here would search for nonsense.
var edition = new Edition { Title = "Stories of Fantasy, Horror/Sci-Fi, and a Man Called Tuf" };

var book = new Book
{
Title = "Dreamsongs",
Editions = new List<Edition> { edition }
};

var title = ReleaseSearchService.GetSearchBookTitle(book, edition);

Assert.That(title, Is.EqualTo("Stories of Fantasy, Horror/Sci-Fi, and a Man Called Tuf"));
}

[Test]
public void should_keep_edition_title_when_no_segment_matches_the_book()
{
// A marketplace-style listing that happens to contain a slash. The book title is not a
// clean segment, so there is nothing safe to fall back to.
var edition = new Edition { Title = "Rare George R R Martin / A KNIGHT OF THE SEVEN KINGDOMS Signed 1st Edition 2015" };

var book = new Book
{
Title = "A Knight of the Seven Kingdoms",
Editions = new List<Edition> { edition }
};

var title = ReleaseSearchService.GetSearchBookTitle(book, edition);

Assert.That(title, Is.EqualTo("Rare George R R Martin / A KNIGHT OF THE SEVEN KINGDOMS Signed 1st Edition 2015"));
}

[Test]
public void should_keep_edition_title_when_book_title_is_blank()
{
var omnibus = new Edition { Title = "A Game of Thrones / A Clash of Kings" };

var book = new Book
{
Title = " ",
Editions = new List<Edition> { omnibus }
};

var title = ReleaseSearchService.GetSearchBookTitle(book, omnibus);

Assert.That(title, Is.EqualTo("A Game of Thrones / A Clash of Kings"));
}

[Test]
public void omnibus_edition_should_produce_a_single_work_book_query()
{
var omnibus = new Edition { Title = "A Game of Thrones / A Clash of Kings" };

var book = new Book
{
Title = "A Clash of Kings",
Editions = new List<Edition> { omnibus }
};

var criteria = new BookSearchCriteria
{
Author = new Author { Name = "George R.R. Martin" },
BookTitle = ReleaseSearchService.GetSearchBookTitle(book, omnibus)
};

Assert.That(criteria.BookQuery, Is.EqualTo("A+Clash+of+Kings"));
}

[Test]
public void book_query_should_use_main_title_section()
{
Expand Down
51 changes: 50 additions & 1 deletion src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Extensions;
Expand All @@ -25,6 +26,8 @@ public interface ISearchForReleases

public class ReleaseSearchService : ISearchForReleases
{
private static readonly Regex CollapsedWhitespace = new Regex(@"\s+", RegexOptions.Compiled);

private readonly IIndexerFactory _indexerFactory;
private readonly IBookService _bookService;
private readonly IAuthorService _authorService;
Expand Down Expand Up @@ -145,12 +148,58 @@ internal static string GetSearchBookTitle(Book book, Edition selectedEdition)
var selectedTitle = selectedEdition?.Title;
if (!string.IsNullOrWhiteSpace(selectedTitle))
{
return selectedTitle;
// An omnibus edition is named after every work it collects ("A Game of Thrones / A
// Clash of Kings"). Searching for that whole string finds nothing, because releases
// are named after a book, not after the compilation that happens to contain it. When
// the book being searched for is itself one of the collected works, search for it by
// name instead of for the compilation.
return GetCollectedWorkTitle(selectedTitle, book.Title) ?? selectedTitle;
}

return book.Title ?? string.Empty;
}

/// <summary>
/// Returns the book's own title when <paramref name="editionTitle"/> is a slash-separated
/// compilation that lists it as one of its works; otherwise null, leaving the caller's title
/// untouched. Deliberately conservative: a slash inside a phrase ("Horror/Sci-Fi") only ever
/// yields segments that fail the equality check, so such titles are left alone.
/// </summary>
internal static string GetCollectedWorkTitle(string editionTitle, string bookTitle)
{
if (string.IsNullOrWhiteSpace(editionTitle) || string.IsNullOrWhiteSpace(bookTitle))
{
return null;
}

var segments = editionTitle.Split('/');
if (segments.Length < 2)
{
return null;
}

var normalisedBookTitle = NormaliseTitleSegment(bookTitle);
if (normalisedBookTitle.Length == 0)
{
return null;
}

foreach (var segment in segments)
{
if (string.Equals(NormaliseTitleSegment(segment), normalisedBookTitle, StringComparison.OrdinalIgnoreCase))
{
return bookTitle.Trim();
}
}

return null;
}

private static string NormaliseTitleSegment(string value)
{
return CollapsedWhitespace.Replace(value ?? string.Empty, " ").Trim();
}

internal static bool HasConfiguredQualityProfileForMediaType(Author author, BookMediaType mediaType)
{
if (author == null)
Expand Down