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/RefreshAuthorService.cs

419 lines
17 KiB

using System;
using System.Collections.Generic;
using System.IO;
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.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.History;
using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
4 years ago
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Books
{
public interface IRefreshAuthorService
{
}
public class RefreshAuthorService : RefreshEntityServiceBase<Author, Book>,
IRefreshAuthorService,
IExecute<RefreshAuthorCommand>,
IExecute<BulkRefreshAuthorCommand>
{
private readonly IProvideAuthorInfo _authorInfo;
private readonly IAuthorService _authorService;
private readonly IBookService _bookService;
4 years ago
private readonly IMetadataProfileService _metadataProfileService;
private readonly IRefreshBookService _refreshBookService;
4 years ago
private readonly IRefreshSeriesService _refreshSeriesService;
private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IRootFolderService _rootFolderService;
private readonly ICheckIfAuthorShouldBeRefreshed _checkIfAuthorShouldBeRefreshed;
private readonly IMonitorNewBookService _monitorNewBookService;
private readonly IConfigService _configService;
private readonly IImportListExclusionService _importListExclusionService;
private readonly Logger _logger;
public RefreshAuthorService(IProvideAuthorInfo authorInfo,
IAuthorService authorService,
IAuthorMetadataService authorMetadataService,
IBookService bookService,
4 years ago
IMetadataProfileService metadataProfileService,
IRefreshBookService refreshBookService,
4 years ago
IRefreshSeriesService refreshSeriesService,
IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager,
IMediaFileService mediaFileService,
IHistoryService historyService,
IRootFolderService rootFolderService,
ICheckIfAuthorShouldBeRefreshed checkIfAuthorShouldBeRefreshed,
IMonitorNewBookService monitorNewBookService,
IConfigService configService,
IImportListExclusionService importListExclusionService,
Logger logger)
: base(logger, authorMetadataService)
{
_authorInfo = authorInfo;
_authorService = authorService;
_bookService = bookService;
4 years ago
_metadataProfileService = metadataProfileService;
_refreshBookService = refreshBookService;
4 years ago
_refreshSeriesService = refreshSeriesService;
_eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager;
_mediaFileService = mediaFileService;
_historyService = historyService;
_rootFolderService = rootFolderService;
_checkIfAuthorShouldBeRefreshed = checkIfAuthorShouldBeRefreshed;
_monitorNewBookService = monitorNewBookService;
_configService = configService;
_importListExclusionService = importListExclusionService;
_logger = logger;
}
private Author GetSkyhookData(string foreignId)
{
try
{
return _authorInfo.GetAuthorInfo(foreignId);
}
catch (AuthorNotFoundException)
{
_logger.Error($"Could not find author with id {foreignId}");
4 years ago
}
return null;
}
protected override RemoteData GetRemoteData(Author local, List<Author> remote, Author data)
{
var result = new RemoteData();
if (data != null)
{
result.Entity = data;
result.Metadata = new List<AuthorMetadata> { data.Metadata.Value };
}
return result;
}
4 years ago
protected override bool ShouldDelete(Author local)
{
return !_mediaFileService.GetFilesByAuthor(local.Id).Any();
}
4 years ago
protected override void LogProgress(Author local)
{
_logger.ProgressInfo("Updating Info for {0}", local.Name);
}
4 years ago
protected override bool IsMerge(Author local, Author remote)
{
4 years ago
_logger.Trace($"local: {local.AuthorMetadataId} remote: {remote.Metadata.Value.Id}");
return local.AuthorMetadataId != remote.Metadata.Value.Id;
}
4 years ago
protected override UpdateResult UpdateEntity(Author local, Author remote)
{
4 years ago
var result = UpdateResult.None;
if (!local.Metadata.Value.Equals(remote.Metadata.Value))
{
result = UpdateResult.UpdateTags;
}
local.UseMetadataFrom(remote);
local.Metadata = remote.Metadata;
4 years ago
local.Series = remote.Series.Value;
local.LastInfoSync = DateTime.UtcNow;
try
{
local.Path = new DirectoryInfo(local.Path).FullName;
local.Path = local.Path.GetActualCasing();
}
catch (Exception e)
{
_logger.Warn(e, "Couldn't update author path for " + local.Path);
}
return result;
}
4 years ago
protected override UpdateResult MoveEntity(Author local, Author remote)
{
4 years ago
_logger.Debug($"Updating foreign id for {local} to {remote}");
// We are moving from one metadata to another (will already have been poplated)
4 years ago
local.AuthorMetadataId = remote.Metadata.Value.Id;
local.Metadata = remote.Metadata.Value;
// Update list exclusion if one exists
4 years ago
var importExclusion = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignAuthorId);
if (importExclusion != null)
{
4 years ago
importExclusion.ForeignId = remote.Metadata.Value.ForeignAuthorId;
_importListExclusionService.Update(importExclusion);
}
// Do the standard update
UpdateEntity(local, remote);
// We know we need to update tags as author id has changed
return UpdateResult.UpdateTags;
}
4 years ago
protected override UpdateResult MergeEntity(Author local, Author target, Author remote)
{
_logger.Warn($"Author {local} was replaced with {remote} because the original was a duplicate.");
// Update list exclusion if one exists
4 years ago
var importExclusionLocal = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignAuthorId);
if (importExclusionLocal != null)
{
4 years ago
var importExclusionTarget = _importListExclusionService.FindByForeignId(target.Metadata.Value.ForeignAuthorId);
if (importExclusionTarget == null)
{
4 years ago
importExclusionLocal.ForeignId = remote.Metadata.Value.ForeignAuthorId;
_importListExclusionService.Update(importExclusionLocal);
}
}
// move any books over to the new author and remove the local author
var books = _bookService.GetBooksByAuthor(local.Id);
books.ForEach(x => x.AuthorMetadataId = target.AuthorMetadataId);
_bookService.UpdateMany(books);
_authorService.DeleteAuthor(local.Id, false);
// Update history entries to new id
var items = _historyService.GetByAuthor(local.Id, null);
4 years ago
items.ForEach(x => x.AuthorId = target.Id);
_historyService.UpdateMany(items);
// We know we need to update tags as author id has changed
return UpdateResult.UpdateTags;
}
4 years ago
protected override Author GetEntityByForeignId(Author local)
{
return _authorService.FindById(local.ForeignAuthorId);
}
4 years ago
protected override void SaveEntity(Author local)
{
_authorService.UpdateAuthor(local);
}
4 years ago
protected override void DeleteEntity(Author local, bool deleteFiles)
{
_authorService.DeleteAuthor(local.Id, true);
}
4 years ago
protected override List<Book> GetRemoteChildren(Author local, Author remote)
{
4 years ago
var filtered = _metadataProfileService.FilterBooks(remote, local.MetadataProfileId);
var all = filtered.DistinctBy(m => m.ForeignBookId).ToList();
var ids = all.Select(x => x.ForeignBookId).ToList();
var excluded = _importListExclusionService.FindByForeignId(ids).Select(x => x.ForeignId).ToList();
4 years ago
return all.Where(x => !excluded.Contains(x.ForeignBookId)).ToList();
}
4 years ago
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
{
return _bookService.GetBooksForRefresh(entity.AuthorMetadataId,
remoteChildren.Select(x => x.ForeignBookId).ToList());
}
4 years ago
protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote)
{
4 years ago
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignBookId == remote.ForeignBookId);
var mergeChildren = new List<Book>();
return Tuple.Create(existingChild, mergeChildren);
}
4 years ago
protected override void PrepareNewChild(Book child, Author entity)
{
4 years ago
child.Author = entity;
child.AuthorMetadata = entity.Metadata.Value;
child.AuthorMetadataId = entity.Metadata.Value.Id;
child.Added = DateTime.UtcNow;
child.LastInfoSync = DateTime.MinValue;
child.Monitored = entity.Monitored;
}
4 years ago
protected override void PrepareExistingChild(Book local, Book remote, Author entity)
{
4 years ago
local.Author = entity;
local.AuthorMetadata = entity.Metadata.Value;
local.AuthorMetadataId = entity.Metadata.Value.Id;
remote.UseDbFieldsFrom(local);
}
protected override void ProcessChildren(Author entity, SortedChildren children)
{
foreach (var book in children.Added)
{
book.Monitored = _monitorNewBookService.ShouldMonitorNewBook(book, children.UpToDate, entity.MonitorNewItems);
}
}
4 years ago
protected override void AddChildren(List<Book> children)
{
_bookService.InsertMany(children);
}
4 years ago
protected override bool RefreshChildren(SortedChildren localChildren, List<Book> remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
{
return _refreshBookService.RefreshBookInfo(localChildren.All, remoteChildren, remoteData, forceChildRefresh, forceUpdateFileTags, lastUpdate);
}
4 years ago
protected override void PublishEntityUpdatedEvent(Author entity)
{
_eventAggregator.PublishEvent(new AuthorUpdatedEvent(entity));
}
4 years ago
protected override void PublishRefreshCompleteEvent(Author entity)
{
4 years ago
// little hack - trigger the series update here
_refreshSeriesService.RefreshSeriesInfo(entity.AuthorMetadataId, entity.Series, entity, false, false, null);
_eventAggregator.PublishEvent(new AuthorRefreshCompleteEvent(entity));
}
protected override void PublishChildrenUpdatedEvent(Author entity, List<Book> newChildren, List<Book> updateChildren, List<Book> deleteChildren)
{
_eventAggregator.PublishEvent(new BookInfoRefreshedEvent(entity, newChildren, updateChildren, deleteChildren));
}
4 years ago
private void Rescan(List<int> authorIds, bool isNew, CommandTrigger trigger, bool infoUpdated)
{
var rescanAfterRefresh = _configService.RescanAfterRefresh;
var shouldRescan = true;
if (isNew)
{
_logger.Trace("Forcing rescan. Reason: New author added");
shouldRescan = true;
}
else if (rescanAfterRefresh == RescanAfterRefreshType.Never)
{
_logger.Trace("Skipping rescan. Reason: never rescan after refresh");
shouldRescan = false;
}
else if (rescanAfterRefresh == RescanAfterRefreshType.AfterManual && trigger != CommandTrigger.Manual)
{
_logger.Trace("Skipping rescan. Reason: not after automatic refreshes");
shouldRescan = false;
}
else if (!infoUpdated)
{
_logger.Trace("Skipping rescan. Reason: no metadata updated");
shouldRescan = false;
}
if (shouldRescan)
{
// some metadata has updated so rescan unmatched
// (but don't add new authors to reduce repeated searches against api)
var folders = _rootFolderService.All().Select(x => x.Path).ToList();
4 years ago
_commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, authorIds));
}
}
private void RefreshSelectedAuthors(List<int> authorIds, bool isNew, CommandTrigger trigger)
{
4 years ago
var updated = false;
var authors = _authorService.GetAuthors(authorIds);
foreach (var author in authors)
{
try
{
var data = GetSkyhookData(author.ForeignAuthorId);
updated |= RefreshEntityInfo(author, null, data, true, false, null);
}
catch (Exception e)
{
_logger.Error(e, "Couldn't refresh info for {0}", author);
}
}
4 years ago
Rescan(authorIds, isNew, trigger, updated);
}
public void Execute(BulkRefreshAuthorCommand message)
{
RefreshSelectedAuthors(message.AuthorIds, message.AreNewAuthors, message.Trigger);
}
public void Execute(RefreshAuthorCommand message)
{
var trigger = message.Trigger;
var isNew = message.IsNewAuthor;
4 years ago
if (message.AuthorId.HasValue)
{
RefreshSelectedAuthors(new List<int> { message.AuthorId.Value }, isNew, trigger);
}
else
{
var updated = false;
var authors = _authorService.GetAllAuthors().OrderBy(c => c.Name).ToList();
var authorIds = authors.Select(x => x.Id).ToList();
var updatedGoodreadsAuthors = new HashSet<string>();
if (message.LastExecutionTime.HasValue && message.LastExecutionTime.Value.AddDays(14) > DateTime.UtcNow)
{
updatedGoodreadsAuthors = _authorInfo.GetChangedAuthors(message.LastStartTime.Value);
}
foreach (var author in authors)
{
var manualTrigger = message.Trigger == CommandTrigger.Manual;
if ((updatedGoodreadsAuthors == null && _checkIfAuthorShouldBeRefreshed.ShouldRefresh(author)) ||
(updatedGoodreadsAuthors != null && updatedGoodreadsAuthors.Contains(author.ForeignAuthorId)) ||
manualTrigger)
{
try
{
LogProgress(author);
var data = GetSkyhookData(author.ForeignAuthorId);
updated |= RefreshEntityInfo(author, null, data, manualTrigger, false, message.LastStartTime);
}
catch (Exception e)
{
_logger.Error(e, "Couldn't refresh info for {0}", author);
}
}
else
{
_logger.Info("Skipping refresh of author: {0}", author.Name);
}
}
4 years ago
Rescan(authorIds, isNew, trigger, updated);
}
}
}
}