mirror of
https://github.com/Readarr/Readarr
synced 2026-01-01 21:24:52 +01:00
parent
9150f6889f
commit
4541d3d3b0
6 changed files with 151 additions and 4 deletions
|
|
@ -187,7 +187,17 @@ public void should_return_rejected_result_for_unparsable_search()
|
|||
_reports[0].Title = "1937 - Snow White and the Seven Dwarves";
|
||||
|
||||
var author = new Author { Name = "Some Author" };
|
||||
var books = new List<Book> { new Book { Title = "Some Book" } };
|
||||
var books = new List<Book>
|
||||
{
|
||||
new Book
|
||||
{
|
||||
Title = "Some Book",
|
||||
Editions = new List<Edition>
|
||||
{
|
||||
new Edition { Title = "Some Edition Title" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Subject.GetSearchDecision(_reports, new BookSearchCriteria { Author = author, Books = books }).ToList();
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ public void Setup()
|
|||
private void GivenSearchCriteria(string authorName, string bookTitle)
|
||||
{
|
||||
_author.Name = authorName;
|
||||
var a = new Book();
|
||||
a.Title = bookTitle;
|
||||
var a = new Book
|
||||
{
|
||||
Title = bookTitle,
|
||||
Editions = new List<Edition> { new Edition { Title = bookTitle, Monitored = true } }
|
||||
};
|
||||
_books.Add(a);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ public interface IEditionRepository : IBasicRepository<Edition>
|
|||
Edition FindByForeignEditionId(string foreignEditionId);
|
||||
List<Edition> FindByBook(int id);
|
||||
List<Edition> FindByAuthor(int id);
|
||||
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
|
||||
Edition FindByTitle(int authorMetadataId, string title);
|
||||
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
|
||||
List<Edition> SetMonitored(Edition edition);
|
||||
}
|
||||
|
|
@ -63,6 +65,28 @@ public List<Edition> FindByAuthor(int id)
|
|||
.Where<Author>(a => a.Id == id));
|
||||
}
|
||||
|
||||
public List<Edition> FindByAuthorMetadataId(int authorMetadataId, bool onlyMonitored)
|
||||
{
|
||||
var builder = Builder().Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Where<Book>(b => b.AuthorMetadataId == authorMetadataId);
|
||||
|
||||
if (onlyMonitored)
|
||||
{
|
||||
builder = builder.Where<Edition>(e => e.Monitored == true);
|
||||
}
|
||||
|
||||
return Query(builder);
|
||||
}
|
||||
|
||||
public Edition FindByTitle(int authorMetadataId, string title)
|
||||
{
|
||||
return Query(Builder().Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Where<Book>(b => b.AuthorMetadataId == authorMetadataId)
|
||||
.Where<Edition>(e => e.Monitored == true)
|
||||
.Where<Edition>(e => e.Title == title))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public List<Edition> SetMonitored(Edition edition)
|
||||
{
|
||||
var allEditions = FindByBook(edition.BookId);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
|
|
@ -16,6 +19,9 @@ public interface IEditionService
|
|||
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
|
||||
List<Edition> GetEditionsByBook(int bookId);
|
||||
List<Edition> GetEditionsByAuthor(int authorId);
|
||||
Edition FindByTitle(int authorMetadataId, string title);
|
||||
Edition FindByTitleInexact(int authorMetadataId, string title);
|
||||
List<Edition> GetCandidates(int authorMetadataId, string title);
|
||||
List<Edition> SetMonitored(Edition edition);
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +87,40 @@ public List<Edition> GetEditionsByAuthor(int authorId)
|
|||
return _editionRepository.FindByAuthor(authorId);
|
||||
}
|
||||
|
||||
public Edition FindByTitle(int authorMetadataId, string title)
|
||||
{
|
||||
return _editionRepository.FindByTitle(authorMetadataId, title);
|
||||
}
|
||||
|
||||
public Edition FindByTitleInexact(int authorMetadataId, string title)
|
||||
{
|
||||
var books = _editionRepository.FindByAuthorMetadataId(authorMetadataId, true);
|
||||
|
||||
foreach (var func in EditionScoringFunctions(title))
|
||||
{
|
||||
var results = FindByStringInexact(books, func.Item1, func.Item2);
|
||||
if (results.Count == 1)
|
||||
{
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Edition> GetCandidates(int authorMetadataId, string title)
|
||||
{
|
||||
var books = _editionRepository.FindByAuthorMetadataId(authorMetadataId, true);
|
||||
var output = new List<Edition>();
|
||||
|
||||
foreach (var func in EditionScoringFunctions(title))
|
||||
{
|
||||
output.AddRange(FindByStringInexact(books, func.Item1, func.Item2));
|
||||
}
|
||||
|
||||
return output.DistinctBy(x => x.Id).ToList();
|
||||
}
|
||||
|
||||
public List<Edition> SetMonitored(Edition edition)
|
||||
{
|
||||
return _editionRepository.SetMonitored(edition);
|
||||
|
|
@ -91,5 +131,40 @@ public void Handle(BookDeletedEvent message)
|
|||
var editions = GetEditionsByBook(message.Book.Id);
|
||||
DeleteMany(editions);
|
||||
}
|
||||
|
||||
private List<Tuple<Func<Edition, string, double>, string>> EditionScoringFunctions(string title)
|
||||
{
|
||||
Func<Func<Edition, string, double>, string, Tuple<Func<Edition, string, double>, string>> tc = Tuple.Create;
|
||||
var scoringFunctions = new List<Tuple<Func<Edition, string, double>, string>>
|
||||
{
|
||||
tc((a, t) => a.Title.FuzzyMatch(t), title),
|
||||
tc((a, t) => a.Title.FuzzyMatch(t), title.RemoveBracketsAndContents().CleanAuthorName()),
|
||||
tc((a, t) => a.Title.FuzzyMatch(t), title.RemoveAfterDash().CleanAuthorName()),
|
||||
tc((a, t) => a.Title.FuzzyMatch(t), title.RemoveBracketsAndContents().RemoveAfterDash().CleanAuthorName()),
|
||||
tc((a, t) => t.FuzzyContains(a.Title), title)
|
||||
};
|
||||
|
||||
return scoringFunctions;
|
||||
}
|
||||
|
||||
private List<Edition> FindByStringInexact(List<Edition> editions, Func<Edition, string, double> scoreFunction, string title)
|
||||
{
|
||||
const double fuzzThreshold = 0.7;
|
||||
const double fuzzGap = 0.4;
|
||||
|
||||
var sortedEditions = editions.Select(s => new
|
||||
{
|
||||
MatchProb = scoreFunction(s, title),
|
||||
Edition = s
|
||||
})
|
||||
.ToList()
|
||||
.OrderByDescending(s => s.MatchProb)
|
||||
.ToList();
|
||||
|
||||
return sortedEditions.TakeWhile((x, i) => i == 0 || sortedEditions[i - 1].MatchProb - x.MatchProb < fuzzGap)
|
||||
.TakeWhile((x, i) => x.MatchProb > fuzzThreshold || (i > 0 && sortedEditions[i - 1].MatchProb > fuzzThreshold))
|
||||
.Select(x => x.Edition)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,7 +351,11 @@ public static ParsedBookInfo ParseBookTitleWithSearchCriteria(string title, Auth
|
|||
|
||||
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
|
||||
|
||||
var bestBook = books.OrderByDescending(x => simpleTitle.FuzzyContains(x.Title)).First();
|
||||
var bestBook = books
|
||||
.OrderByDescending(x => simpleTitle.FuzzyContains(x.Editions.Value.Single(x => x.Monitored).Title))
|
||||
.First()
|
||||
.Editions.Value
|
||||
.Single(x => x.Monitored);
|
||||
|
||||
var foundAuthor = GetTitleFuzzy(simpleTitle, authorName, out var remainder);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,15 +28,18 @@ public class ParsingService : IParsingService
|
|||
{
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IEditionService _editionService;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ParsingService(IAuthorService authorService,
|
||||
IBookService bookService,
|
||||
IEditionService editionService,
|
||||
IMediaFileService mediaFileService,
|
||||
Logger logger)
|
||||
{
|
||||
_bookService = bookService;
|
||||
_editionService = editionService;
|
||||
_authorService = authorService;
|
||||
_mediaFileService = mediaFileService;
|
||||
_logger = logger;
|
||||
|
|
@ -127,12 +130,25 @@ public List<Book> GetBooks(ParsedBookInfo parsedBookInfo, Author author, SearchC
|
|||
bookInfo = _bookService.FindByTitle(author.AuthorMetadataId, parsedBookInfo.BookTitle);
|
||||
}
|
||||
|
||||
if (bookInfo == null)
|
||||
{
|
||||
var edition = _editionService.FindByTitle(author.AuthorMetadataId, parsedBookInfo.BookTitle);
|
||||
bookInfo = edition?.Book.Value;
|
||||
}
|
||||
|
||||
if (bookInfo == null)
|
||||
{
|
||||
_logger.Debug("Trying inexact book match for {0}", parsedBookInfo.BookTitle);
|
||||
bookInfo = _bookService.FindByTitleInexact(author.AuthorMetadataId, parsedBookInfo.BookTitle);
|
||||
}
|
||||
|
||||
if (bookInfo == null)
|
||||
{
|
||||
_logger.Debug("Trying inexact edition match for {0}", parsedBookInfo.BookTitle);
|
||||
var edition = _editionService.FindByTitleInexact(author.AuthorMetadataId, parsedBookInfo.BookTitle);
|
||||
bookInfo = edition?.Book.Value;
|
||||
}
|
||||
|
||||
if (bookInfo != null)
|
||||
{
|
||||
result.Add(bookInfo);
|
||||
|
|
@ -213,6 +229,21 @@ public ParsedBookInfo ParseBookTitleFuzzy(string title)
|
|||
bestBook = book;
|
||||
}
|
||||
}
|
||||
|
||||
var possibleEditions = _editionService.GetCandidates(author.AuthorMetadataId, title);
|
||||
foreach (var edition in possibleEditions)
|
||||
{
|
||||
var editionMatch = title.FuzzyMatch(edition.Title, 0.5);
|
||||
var score = (authorMatch.Item2 + editionMatch.Item2) / 2;
|
||||
|
||||
_logger.Trace($"Edition {edition} has score {score}");
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestAuthor = author;
|
||||
bestBook = edition.Book.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Trace($"Best match: {bestAuthor} {bestBook}");
|
||||
|
|
|
|||
Loading…
Reference in a new issue