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.
314 lines
11 KiB
314 lines
11 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using NLog;
|
|
using NzbDrone.Common.Extensions;
|
|
using NzbDrone.Core.Books.Events;
|
|
using NzbDrone.Core.Datastore;
|
|
using NzbDrone.Core.Messaging.Events;
|
|
using NzbDrone.Core.Parser;
|
|
|
|
namespace NzbDrone.Core.Books
|
|
{
|
|
public interface IBookService
|
|
{
|
|
Book GetBook(int bookId);
|
|
List<Book> GetBooks(IEnumerable<int> bookIds);
|
|
List<Book> GetBooksByAuthor(int authorId);
|
|
List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
|
List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
|
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
|
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds);
|
|
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
|
Book AddBook(Book newBook, bool doRefresh = true);
|
|
Book FindById(string foreignId);
|
|
Book FindBySlug(string titleSlug);
|
|
Book FindByTitle(int authorMetadataId, string title);
|
|
Book FindByTitleInexact(int authorMetadataId, string title);
|
|
List<Book> GetCandidates(int authorMetadataId, string title);
|
|
void DeleteBook(int bookId, bool deleteFiles, bool addImportListExclusion = false);
|
|
List<Book> GetAllBooks();
|
|
Book UpdateBook(Book book);
|
|
void SetBookMonitored(int bookId, bool monitored);
|
|
void SetMonitored(IEnumerable<int> ids, bool monitored);
|
|
PagingSpec<Book> BooksWithoutFiles(PagingSpec<Book> pagingSpec);
|
|
List<Book> BooksBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
|
|
List<Book> AuthorBooksBetweenDates(Author author, DateTime start, DateTime end, bool includeUnmonitored);
|
|
void InsertMany(List<Book> books);
|
|
void UpdateMany(List<Book> books);
|
|
void DeleteMany(List<Book> books);
|
|
void SetAddOptions(IEnumerable<Book> books);
|
|
List<Book> GetAuthorBooksWithFiles(Author author);
|
|
}
|
|
|
|
public class BookService : IBookService,
|
|
IHandle<AuthorDeletedEvent>
|
|
{
|
|
private readonly IBookRepository _bookRepository;
|
|
private readonly IEditionService _editionService;
|
|
private readonly IEventAggregator _eventAggregator;
|
|
private readonly Logger _logger;
|
|
|
|
public BookService(IBookRepository bookRepository,
|
|
IEditionService editionService,
|
|
IEventAggregator eventAggregator,
|
|
Logger logger)
|
|
{
|
|
_bookRepository = bookRepository;
|
|
_editionService = editionService;
|
|
_eventAggregator = eventAggregator;
|
|
_logger = logger;
|
|
}
|
|
|
|
public Book AddBook(Book newBook, bool doRefresh = true)
|
|
{
|
|
if (newBook.AuthorMetadataId == 0)
|
|
{
|
|
throw new InvalidOperationException("Cannot insert book with AuthorMetadataId = 0");
|
|
}
|
|
|
|
_bookRepository.Upsert(newBook);
|
|
|
|
var editions = newBook.Editions.Value;
|
|
editions.ForEach(x => x.BookId = newBook.Id);
|
|
|
|
_editionService.InsertMany(editions.Where(x => x.Id == 0).ToList());
|
|
_editionService.SetMonitored(editions.FirstOrDefault(x => x.Monitored) ?? editions.First());
|
|
|
|
_eventAggregator.PublishEvent(new BookAddedEvent(GetBook(newBook.Id), doRefresh));
|
|
|
|
return newBook;
|
|
}
|
|
|
|
public void DeleteBook(int bookId, bool deleteFiles, bool addImportListExclusion = false)
|
|
{
|
|
var book = _bookRepository.Get(bookId);
|
|
book.Author.LazyLoad();
|
|
_bookRepository.Delete(bookId);
|
|
_eventAggregator.PublishEvent(new BookDeletedEvent(book, deleteFiles, addImportListExclusion));
|
|
}
|
|
|
|
public Book FindById(string foreignId)
|
|
{
|
|
return _bookRepository.FindById(foreignId);
|
|
}
|
|
|
|
public Book FindBySlug(string titleSlug)
|
|
{
|
|
return _bookRepository.FindBySlug(titleSlug);
|
|
}
|
|
|
|
public Book FindByTitle(int authorMetadataId, string title)
|
|
{
|
|
return _bookRepository.FindByTitle(authorMetadataId, title);
|
|
}
|
|
|
|
private List<Tuple<Func<Book, string, double>, string>> BookScoringFunctions(string title, string cleanTitle)
|
|
{
|
|
Func<Func<Book, string, double>, string, Tuple<Func<Book, string, double>, string>> tc = Tuple.Create;
|
|
var scoringFunctions = new List<Tuple<Func<Book, string, double>, 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().CleanAuthorName()),
|
|
tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveAfterDash().CleanAuthorName()),
|
|
tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveBracketsAndContents().RemoveAfterDash().CleanAuthorName()),
|
|
tc((a, t) => t.FuzzyContains(a.CleanTitle), cleanTitle),
|
|
tc((a, t) => t.FuzzyContains(a.Title), title),
|
|
tc((a, t) => a.Title.SplitBookTitle(a.AuthorMetadata.Value.Name).Item1.FuzzyMatch(t), title)
|
|
};
|
|
|
|
return scoringFunctions;
|
|
}
|
|
|
|
public Book FindByTitleInexact(int authorMetadataId, string title)
|
|
{
|
|
var books = GetBooksByAuthorMetadataId(authorMetadataId);
|
|
|
|
foreach (var func in BookScoringFunctions(title, title.CleanAuthorName()))
|
|
{
|
|
var results = FindByStringInexact(books, func.Item1, func.Item2);
|
|
if (results.Count == 1)
|
|
{
|
|
return results[0];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public List<Book> GetCandidates(int authorMetadataId, string title)
|
|
{
|
|
var books = GetBooksByAuthorMetadataId(authorMetadataId);
|
|
var output = new List<Book>();
|
|
|
|
foreach (var func in BookScoringFunctions(title, title.CleanAuthorName()))
|
|
{
|
|
output.AddRange(FindByStringInexact(books, func.Item1, func.Item2));
|
|
}
|
|
|
|
return output.DistinctBy(x => x.Id).ToList();
|
|
}
|
|
|
|
private List<Book> FindByStringInexact(List<Book> books, Func<Book, string, double> scoreFunction, string title)
|
|
{
|
|
const double fuzzThreshold = 0.7;
|
|
const double fuzzGap = 0.4;
|
|
|
|
var sortedBooks = books.Select(s => new
|
|
{
|
|
MatchProb = scoreFunction(s, title),
|
|
Book = s
|
|
})
|
|
.ToList()
|
|
.OrderByDescending(s => s.MatchProb)
|
|
.ToList();
|
|
|
|
return sortedBooks.TakeWhile((x, i) => i == 0 || sortedBooks[i - 1].MatchProb - x.MatchProb < fuzzGap)
|
|
.TakeWhile((x, i) => x.MatchProb > fuzzThreshold || (i > 0 && sortedBooks[i - 1].MatchProb > fuzzThreshold))
|
|
.Select(x => x.Book)
|
|
.ToList();
|
|
}
|
|
|
|
public List<Book> GetAllBooks()
|
|
{
|
|
return _bookRepository.All().ToList();
|
|
}
|
|
|
|
public Book GetBook(int bookId)
|
|
{
|
|
return _bookRepository.Get(bookId);
|
|
}
|
|
|
|
public List<Book> GetBooks(IEnumerable<int> bookIds)
|
|
{
|
|
return _bookRepository.Get(bookIds).ToList();
|
|
}
|
|
|
|
public List<Book> GetBooksByAuthor(int authorId)
|
|
{
|
|
return _bookRepository.GetBooks(authorId).ToList();
|
|
}
|
|
|
|
public List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds)
|
|
{
|
|
return _bookRepository.GetNextBooks(authorMetadataIds).ToList();
|
|
}
|
|
|
|
public List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds)
|
|
{
|
|
return _bookRepository.GetLastBooks(authorMetadataIds).ToList();
|
|
}
|
|
|
|
public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
|
|
{
|
|
return _bookRepository.GetBooksByAuthorMetadataId(authorMetadataId).ToList();
|
|
}
|
|
|
|
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds)
|
|
{
|
|
return _bookRepository.GetBooksForRefresh(authorMetadataId, foreignIds);
|
|
}
|
|
|
|
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
|
|
{
|
|
return _bookRepository.GetBooksByFileIds(fileIds);
|
|
}
|
|
|
|
public void SetAddOptions(IEnumerable<Book> books)
|
|
{
|
|
_bookRepository.SetFields(books.ToList(), s => s.AddOptions);
|
|
}
|
|
|
|
public PagingSpec<Book> BooksWithoutFiles(PagingSpec<Book> pagingSpec)
|
|
{
|
|
var bookResult = _bookRepository.BooksWithoutFiles(pagingSpec);
|
|
|
|
return bookResult;
|
|
}
|
|
|
|
public List<Book> BooksBetweenDates(DateTime start, DateTime end, bool includeUnmonitored)
|
|
{
|
|
var books = _bookRepository.BooksBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored);
|
|
|
|
return books;
|
|
}
|
|
|
|
public List<Book> AuthorBooksBetweenDates(Author author, DateTime start, DateTime end, bool includeUnmonitored)
|
|
{
|
|
var books = _bookRepository.AuthorBooksBetweenDates(author, start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored);
|
|
|
|
return books;
|
|
}
|
|
|
|
public List<Book> GetAuthorBooksWithFiles(Author author)
|
|
{
|
|
return _bookRepository.GetAuthorBooksWithFiles(author);
|
|
}
|
|
|
|
public void InsertMany(List<Book> books)
|
|
{
|
|
if (books.Any(x => x.AuthorMetadataId == 0))
|
|
{
|
|
throw new InvalidOperationException("Cannot insert book with AuthorMetadataId = 0");
|
|
}
|
|
|
|
_bookRepository.InsertMany(books);
|
|
}
|
|
|
|
public void UpdateMany(List<Book> books)
|
|
{
|
|
_bookRepository.UpdateMany(books);
|
|
}
|
|
|
|
public void DeleteMany(List<Book> books)
|
|
{
|
|
_bookRepository.DeleteMany(books);
|
|
|
|
foreach (var book in books)
|
|
{
|
|
_eventAggregator.PublishEvent(new BookDeletedEvent(book, false, false));
|
|
}
|
|
}
|
|
|
|
public Book UpdateBook(Book book)
|
|
{
|
|
var storedBook = GetBook(book.Id);
|
|
var updatedBook = _bookRepository.Update(book);
|
|
|
|
_eventAggregator.PublishEvent(new BookEditedEvent(updatedBook, storedBook));
|
|
|
|
return updatedBook;
|
|
}
|
|
|
|
public void SetBookMonitored(int bookId, bool monitored)
|
|
{
|
|
var book = _bookRepository.Get(bookId);
|
|
_bookRepository.SetMonitoredFlat(book, monitored);
|
|
|
|
// publish book edited event so author stats update
|
|
_eventAggregator.PublishEvent(new BookEditedEvent(book, book));
|
|
|
|
_logger.Debug("Monitored flag for Book:{0} was set to {1}", bookId, monitored);
|
|
}
|
|
|
|
public void SetMonitored(IEnumerable<int> ids, bool monitored)
|
|
{
|
|
_bookRepository.SetMonitored(ids, monitored);
|
|
|
|
// publish book edited event so author stats update
|
|
foreach (var book in _bookRepository.Get(ids))
|
|
{
|
|
_eventAggregator.PublishEvent(new BookEditedEvent(book, book));
|
|
}
|
|
}
|
|
|
|
public void Handle(AuthorDeletedEvent message)
|
|
{
|
|
var books = GetBooksByAuthorMetadataId(message.Author.AuthorMetadataId);
|
|
DeleteMany(books);
|
|
}
|
|
}
|
|
}
|