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

386 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;
using NzbDrone.Core.RootFolders;
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 IRootFolderService _rootFolderService;
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,
IRootFolderService rootFolderService,
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;
_rootFolderService = rootFolderService;
_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)
{
try
{
var tuple = _bookInfo.GetBookInfo(book.ForeignBookId);
var author = _authorInfo.GetAuthorInfo(tuple.Item1);
var newbook = tuple.Item2;
newbook.Author = author;
newbook.AuthorMetadata = author.Metadata.Value;
newbook.AuthorMetadataId = book.AuthorMetadataId;
newbook.AuthorMetadata.Value.Id = book.AuthorMetadataId;
author.Books = new List<Book> { newbook };
return author;
}
catch (BookNotFoundException)
{
_logger.Error($"Could not find book with id {book.ForeignBookId}");
}
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)
{
data = GetSkyhookData(local);
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 = _rootFolderService.GetBestRootFolderPath(oldAuthor.Path),
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).ToList());
}
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);
}
}
}
}