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.
Readarr/src/NzbDrone.Core/Books/Services/RefreshBookService.cs

390 lines
15 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Books.Commands;
using NzbDrone.Core.Books.Events;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
namespace NzbDrone.Core.Books
{
public interface IRefreshBookService
{
bool RefreshBookInfo(Book book, List<Book> remoteBooks, Author remoteData, bool forceUpdateFileTags);
bool RefreshBookInfo(List<Book> books, List<Book> remoteBooks, Author remoteData, bool forceBookRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
}
public class RefreshBookService : RefreshEntityServiceBase<Book, Edition>,
IRefreshBookService,
IExecute<RefreshBookCommand>,
IExecute<BulkRefreshBookCommand>
{
private readonly IBookService _bookService;
private readonly IAuthorService _authorService;
private readonly IAddAuthorService _addAuthorService;
private readonly IEditionService _editionService;
private readonly IProvideAuthorInfo _authorInfo;
private readonly IProvideBookInfo _bookInfo;
private readonly IRefreshEditionService _refreshEditionService;
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator;
private readonly ICheckIfBookShouldBeRefreshed _checkIfBookShouldBeRefreshed;
private readonly IMapCoversToLocal _mediaCoverService;
private readonly Logger _logger;
public RefreshBookService(IBookService bookService,
IAuthorService authorService,
IAddAuthorService addAuthorService,
IEditionService editionService,
IAuthorMetadataService authorMetadataService,
IProvideAuthorInfo authorInfo,
IProvideBookInfo bookInfo,
IRefreshEditionService refreshEditionService,
IMediaFileService mediaFileService,
IHistoryService historyService,
IEventAggregator eventAggregator,
ICheckIfBookShouldBeRefreshed checkIfBookShouldBeRefreshed,
IMapCoversToLocal mediaCoverService,
Logger logger)
: base(logger, authorMetadataService)
{
_bookService = bookService;
_authorService = authorService;
_addAuthorService = addAuthorService;
_editionService = editionService;
_authorInfo = authorInfo;
_bookInfo = bookInfo;
_refreshEditionService = refreshEditionService;
_mediaFileService = mediaFileService;
_historyService = historyService;
_eventAggregator = eventAggregator;
_checkIfBookShouldBeRefreshed = checkIfBookShouldBeRefreshed;
_mediaCoverService = mediaCoverService;
_logger = logger;
}
private Author GetSkyhookData(Book book)
{
var foreignId = book.Editions.Value.First().ForeignEditionId;
try
{
var tuple = _bookInfo.GetBookInfo(foreignId, false);
var author = _authorInfo.GetAuthorInfo(tuple.Item1, false);
var newbook = tuple.Item2;
newbook.Author = author;
newbook.AuthorMetadata = author.Metadata.Value;
newbook.AuthorMetadataId = book.AuthorMetadataId;
newbook.AuthorMetadata.Value.Id = book.AuthorMetadataId;
// make sure to grab editions data for any other existing editions
foreach (var edition in book.Editions.Value.Skip(1))
{
tuple = _bookInfo.GetBookInfo(edition.ForeignEditionId, false);
newbook.Editions.Value.AddRange(tuple.Item2.Editions.Value);
}
author.Books = new List<Book> { newbook };
return author;
}
catch (BookNotFoundException)
{
_logger.Error($"Could not find book with id {foreignId}");
}
return null;
}
protected override RemoteData GetRemoteData(Book local, List<Book> remote, Author data)
{
var result = new RemoteData();
var book = remote.SingleOrDefault(x => x.ForeignBookId == local.ForeignBookId);
if (book == null && ShouldDelete(local))
{
return result;
}
if (book == null)
{
book = data.Books.Value.SingleOrDefault(x => x.ForeignBookId == local.ForeignBookId);
}
result.Entity = book;
if (result.Entity != null)
{
result.Entity.Id = local.Id;
}
return result;
}
protected override void EnsureNewParent(Book local, Book remote)
{
// Make sure the appropriate author exists (it could be that an book changes parent)
// The authorMetadata entry will be in the db but make sure a corresponding author is too
// so that the book doesn't just disappear.
// TODO filter by metadata id before hitting database
_logger.Trace($"Ensuring parent author exists [{remote.AuthorMetadata.Value.ForeignAuthorId}]");
var newAuthor = _authorService.FindById(remote.AuthorMetadata.Value.ForeignAuthorId);
if (newAuthor == null)
{
var oldAuthor = local.Author.Value;
var addAuthor = new Author
{
Metadata = remote.AuthorMetadata.Value,
MetadataProfileId = oldAuthor.MetadataProfileId,
QualityProfileId = oldAuthor.QualityProfileId,
RootFolderPath = oldAuthor.RootFolderPath,
Monitored = oldAuthor.Monitored,
Tags = oldAuthor.Tags
};
_logger.Debug($"Adding missing parent author {addAuthor}");
_addAuthorService.AddAuthor(addAuthor);
}
}
protected override bool ShouldDelete(Book local)
{
// not manually added and has no files
return local.AddOptions.AddType != BookAddType.Manual &&
!_mediaFileService.GetFilesByBook(local.Id).Any();
}
protected override void LogProgress(Book local)
{
_logger.ProgressInfo("Updating Info for {0}", local.Title);
}
protected override bool IsMerge(Book local, Book remote)
{
return local.ForeignBookId != remote.ForeignBookId;
}
protected override UpdateResult UpdateEntity(Book local, Book remote)
{
UpdateResult result;
remote.UseDbFieldsFrom(local);
if (local.Title != (remote.Title ?? "Unknown") ||
local.ForeignBookId != remote.ForeignBookId ||
local.AuthorMetadata.Value.ForeignAuthorId != remote.AuthorMetadata.Value.ForeignAuthorId)
{
result = UpdateResult.UpdateTags;
}
else if (!local.Equals(remote))
{
result = UpdateResult.Standard;
}
else
{
result = UpdateResult.None;
}
// Force update and fetch covers if images have changed so that we can write them into tags
// if (remote.Images.Any() && !local.Images.SequenceEqual(remote.Images))
// {
// _mediaCoverService.EnsureBookCovers(remote);
// result = UpdateResult.UpdateTags;
// }
local.UseMetadataFrom(remote);
local.AuthorMetadataId = remote.AuthorMetadata.Value.Id;
local.LastInfoSync = DateTime.UtcNow;
return result;
}
protected override UpdateResult MergeEntity(Book local, Book target, Book remote)
{
_logger.Warn($"Book {local} was merged with {remote} because the original was a duplicate.");
// Update book ids for trackfiles
var files = _mediaFileService.GetFilesByBook(local.Id);
files.ForEach(x => x.EditionId = target.Editions.Value.Single(e => e.Monitored).Id);
_mediaFileService.Update(files);
// Update book ids for history
var items = _historyService.GetByBook(local.Id, null);
items.ForEach(x => x.BookId = target.Id);
_historyService.UpdateMany(items);
// Finally delete the old book
_bookService.DeleteMany(new List<Book> { local });
return UpdateResult.UpdateTags;
}
protected override Book GetEntityByForeignId(Book local)
{
return _bookService.FindById(local.ForeignBookId);
}
protected override void SaveEntity(Book local)
{
// Use UpdateMany to avoid firing the book edited event
_bookService.UpdateMany(new List<Book> { local });
}
protected override void DeleteEntity(Book local, bool deleteFiles)
{
_bookService.DeleteBook(local.Id, true);
}
protected override List<Edition> GetRemoteChildren(Book local, Book remote)
{
return remote.Editions.Value.DistinctBy(m => m.ForeignEditionId).ToList();
}
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
{
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId));
}
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)
{
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignEditionId == remote.ForeignEditionId);
return Tuple.Create(existingChild, new List<Edition>());
}
protected override void PrepareNewChild(Edition child, Book entity)
{
child.BookId = entity.Id;
child.Book = entity;
}
protected override void PrepareExistingChild(Edition local, Edition remote, Book entity)
{
local.BookId = entity.Id;
local.Book = entity;
remote.UseDbFieldsFrom(local);
}
protected override void AddChildren(List<Edition> children)
{
// hack - add the chilren in refresh children so we can control monitored status
}
private void MonitorSingleEdition(SortedChildren children)
{
children.Old.ForEach(x => x.Monitored = false);
var monitored = children.Future.Where(x => x.Monitored).ToList();
if (monitored.Count == 1)
{
return;
}
if (monitored.Count == 0)
{
monitored = children.Future;
}
if (monitored.Count == 0)
{
// there are no future children so nothing to do
return;
}
var toMonitor = monitored.OrderByDescending(x => x.Id > 0 ? _mediaFileService.GetFilesByEdition(x.Id).Count : 0)
.ThenByDescending(x => x.Ratings.Popularity).First();
monitored.ForEach(x => x.Monitored = false);
toMonitor.Monitored = true;
// force update of anything we've messed with
var extraToUpdate = children.UpToDate.Where(x => monitored.Contains(x));
children.UpToDate = children.UpToDate.Except(extraToUpdate).ToList();
children.Updated.AddRange(extraToUpdate);
Debug.Assert(!children.Future.Any() || children.Future.Count(x => x.Monitored) == 1, "one edition monitored");
}
protected override bool RefreshChildren(SortedChildren localChildren, List<Edition> remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
{
// make sure only one of the releases ends up monitored
MonitorSingleEdition(localChildren);
localChildren.All.ForEach(x => _logger.Trace($"release: {x} monitored: {x.Monitored}"));
_editionService.InsertMany(localChildren.Added);
return _refreshEditionService.RefreshEditionInfo(localChildren.Added, localChildren.Updated, localChildren.Merged, localChildren.Deleted, localChildren.UpToDate, remoteChildren, forceUpdateFileTags);
}
protected override void PublishEntityUpdatedEvent(Book entity)
{
// Fetch fresh from DB so all lazy loads are available
_eventAggregator.PublishEvent(new BookUpdatedEvent(_bookService.GetBook(entity.Id)));
}
public bool RefreshBookInfo(List<Book> books, List<Book> remoteBooks, Author remoteData, bool forceBookRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
{
var updated = false;
foreach (var book in books)
{
if (forceBookRefresh || _checkIfBookShouldBeRefreshed.ShouldRefresh(book))
{
updated |= RefreshBookInfo(book, remoteBooks, remoteData, forceUpdateFileTags);
}
else
{
_logger.Debug("Skipping refresh of book: {0}", book.Title);
}
}
return updated;
}
public bool RefreshBookInfo(Book book, List<Book> remoteBooks, Author remoteData, bool forceUpdateFileTags)
{
return RefreshEntityInfo(book, remoteBooks, remoteData, true, forceUpdateFileTags, null);
}
public bool RefreshBookInfo(Book book)
{
var data = GetSkyhookData(book);
return RefreshBookInfo(book, data.Books, data, false);
}
public void Execute(BulkRefreshBookCommand message)
{
var books = _bookService.GetBooks(message.BookIds);
foreach (var book in books)
{
RefreshBookInfo(book);
}
}
public void Execute(RefreshBookCommand message)
{
if (message.BookId.HasValue)
{
var book = _bookService.GetBook(message.BookId.Value);
RefreshBookInfo(book);
}
}
}
}