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

4 years ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
4 years ago
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
4 years ago
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Books.Commands;
using NzbDrone.Core.Books.Events;
using NzbDrone.Core.Exceptions;
4 years ago
using NzbDrone.Core.History;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Commands;
4 years ago
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.RootFolders;
4 years ago
namespace NzbDrone.Core.Books
4 years ago
{
public interface IRefreshBookService
4 years ago
{
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);
4 years ago
}
public class RefreshBookService : RefreshEntityServiceBase<Book, Edition>,
IRefreshBookService,
IExecute<RefreshBookCommand>,
IExecute<BulkRefreshBookCommand>
4 years ago
{
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;
4 years ago
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator;
private readonly ICheckIfBookShouldBeRefreshed _checkIfBookShouldBeRefreshed;
4 years ago
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)
4 years ago
{
_bookService = bookService;
_authorService = authorService;
_rootFolderService = rootFolderService;
_addAuthorService = addAuthorService;
_editionService = editionService;
_authorInfo = authorInfo;
_bookInfo = bookInfo;
_refreshEditionService = refreshEditionService;
4 years ago
_mediaFileService = mediaFileService;
_historyService = historyService;
_eventAggregator = eventAggregator;
_checkIfBookShouldBeRefreshed = checkIfBookShouldBeRefreshed;
4 years ago
_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;
}
4 years ago
protected override RemoteData GetRemoteData(Book local, List<Book> remote, Author data)
{
var result = new RemoteData();
var book = remote.SingleOrDefault(x => x.ForeignBookId == local.ForeignBookId);
4 years ago
if (book == null && ShouldDelete(local))
{
return result;
}
if (book == null)
{
data = GetSkyhookData(local);
book = data.Books.Value.SingleOrDefault(x => x.ForeignBookId == local.ForeignBookId);
4 years ago
}
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.
4 years ago
// TODO filter by metadata id before hitting database
_logger.Trace($"Ensuring parent author exists [{remote.AuthorMetadata.Value.ForeignAuthorId}]");
4 years ago
var newAuthor = _authorService.FindById(remote.AuthorMetadata.Value.ForeignAuthorId);
4 years ago
if (newAuthor == null)
4 years ago
{
var oldAuthor = local.Author.Value;
var addAuthor = new Author
4 years ago
{
Metadata = remote.AuthorMetadata.Value,
MetadataProfileId = oldAuthor.MetadataProfileId,
QualityProfileId = oldAuthor.QualityProfileId,
RootFolderPath = _rootFolderService.GetBestRootFolderPath(oldAuthor.Path),
Monitored = oldAuthor.Monitored,
Tags = oldAuthor.Tags
4 years ago
};
_logger.Debug($"Adding missing parent author {addAuthor}");
_addAuthorService.AddAuthor(addAuthor);
4 years ago
}
}
protected override bool ShouldDelete(Book local)
{
// not manually added and has no files
return local.AddOptions.AddType != BookAddType.Manual &&
!_mediaFileService.GetFilesByBook(local.Id).Any();
4 years ago
}
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);
4 years ago
// 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.");
4 years ago
// Update book ids for trackfiles
var files = _mediaFileService.GetFilesByBook(local.Id);
files.ForEach(x => x.EditionId = target.Editions.Value.Single(e => e.Monitored).Id);
4 years ago
_mediaFileService.Update(files);
// Update book ids for history
var items = _historyService.GetByBook(local.Id, null);
4 years ago
items.ForEach(x => x.BookId = target.Id);
_historyService.UpdateMany(items);
// Finally delete the old book
_bookService.DeleteMany(new List<Book> { local });
4 years ago
return UpdateResult.UpdateTags;
}
protected override Book GetEntityByForeignId(Book local)
{
return _bookService.FindById(local.ForeignBookId);
4 years ago
}
protected override void SaveEntity(Book local)
{
// Use UpdateMany to avoid firing the book edited event
_bookService.UpdateMany(new List<Book> { local });
4 years ago
}
protected override void DeleteEntity(Book local, bool deleteFiles)
{
_bookService.DeleteBook(local.Id, true);
4 years ago
}
protected override List<Edition> GetRemoteChildren(Book local, Book remote)
4 years ago
{
return remote.Editions.Value.DistinctBy(m => m.ForeignEditionId).ToList();
4 years ago
}
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
4 years ago
{
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId).ToList());
4 years ago
}
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)
4 years ago
{
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignEditionId == remote.ForeignEditionId);
return Tuple.Create(existingChild, new List<Edition>());
4 years ago
}
protected override void PrepareNewChild(Edition child, Book entity)
4 years ago
{
child.BookId = entity.Id;
child.Book = entity;
4 years ago
}
protected override void PrepareExistingChild(Edition local, Edition remote, Book entity)
4 years ago
{
local.BookId = entity.Id;
local.Book = entity;
remote.UseDbFieldsFrom(local);
4 years ago
}
protected override void AddChildren(List<Edition> children)
4 years ago
{
// hack - add the chilren in refresh children so we can control monitored status
4 years ago
}
private void MonitorSingleEdition(SortedChildren children)
4 years ago
{
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);
4 years ago
}
protected override void PublishEntityUpdatedEvent(Book entity)
{
// Fetch fresh from DB so all lazy loads are available
_eventAggregator.PublishEvent(new BookUpdatedEvent(_bookService.GetBook(entity.Id)));
4 years ago
}
public bool RefreshBookInfo(List<Book> books, List<Book> remoteBooks, Author remoteData, bool forceBookRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
4 years ago
{
var updated = false;
foreach (var book in books)
4 years ago
{
if (forceBookRefresh || _checkIfBookShouldBeRefreshed.ShouldRefresh(book))
4 years ago
{
updated |= RefreshBookInfo(book, remoteBooks, remoteData, forceUpdateFileTags);
4 years ago
}
else
{
_logger.Debug("Skipping refresh of book: {0}", book.Title);
4 years ago
}
}
return updated;
}
public bool RefreshBookInfo(Book book, List<Book> remoteBooks, Author remoteData, bool forceUpdateFileTags)
4 years ago
{
return RefreshEntityInfo(book, remoteBooks, remoteData, true, forceUpdateFileTags, null);
4 years ago
}
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);
}
}
4 years ago
}
}