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.
419 lines
17 KiB
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;
|
|
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;
|
|
private readonly IMetadataProfileService _metadataProfileService;
|
|
private readonly IRefreshBookService _refreshBookService;
|
|
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,
|
|
IMetadataProfileService metadataProfileService,
|
|
IRefreshBookService refreshBookService,
|
|
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;
|
|
_metadataProfileService = metadataProfileService;
|
|
_refreshBookService = refreshBookService;
|
|
_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}");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
protected override bool ShouldDelete(Author local)
|
|
{
|
|
return !_mediaFileService.GetFilesByAuthor(local.Id).Any();
|
|
}
|
|
|
|
protected override void LogProgress(Author local)
|
|
{
|
|
_logger.ProgressInfo("Updating Info for {0}", local.Name);
|
|
}
|
|
|
|
protected override bool IsMerge(Author local, Author remote)
|
|
{
|
|
_logger.Trace($"local: {local.AuthorMetadataId} remote: {remote.Metadata.Value.Id}");
|
|
return local.AuthorMetadataId != remote.Metadata.Value.Id;
|
|
}
|
|
|
|
protected override UpdateResult UpdateEntity(Author local, Author remote)
|
|
{
|
|
var result = UpdateResult.None;
|
|
|
|
if (!local.Metadata.Value.Equals(remote.Metadata.Value))
|
|
{
|
|
result = UpdateResult.UpdateTags;
|
|
}
|
|
|
|
local.UseMetadataFrom(remote);
|
|
local.Metadata = remote.Metadata;
|
|
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;
|
|
}
|
|
|
|
protected override UpdateResult MoveEntity(Author local, Author remote)
|
|
{
|
|
_logger.Debug($"Updating foreign id for {local} to {remote}");
|
|
|
|
// We are moving from one metadata to another (will already have been poplated)
|
|
local.AuthorMetadataId = remote.Metadata.Value.Id;
|
|
local.Metadata = remote.Metadata.Value;
|
|
|
|
// Update list exclusion if one exists
|
|
var importExclusion = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignAuthorId);
|
|
|
|
if (importExclusion != null)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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
|
|
var importExclusionLocal = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignAuthorId);
|
|
|
|
if (importExclusionLocal != null)
|
|
{
|
|
var importExclusionTarget = _importListExclusionService.FindByForeignId(target.Metadata.Value.ForeignAuthorId);
|
|
if (importExclusionTarget == null)
|
|
{
|
|
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);
|
|
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;
|
|
}
|
|
|
|
protected override Author GetEntityByForeignId(Author local)
|
|
{
|
|
return _authorService.FindById(local.ForeignAuthorId);
|
|
}
|
|
|
|
protected override void SaveEntity(Author local)
|
|
{
|
|
_authorService.UpdateAuthor(local);
|
|
}
|
|
|
|
protected override void DeleteEntity(Author local, bool deleteFiles)
|
|
{
|
|
_authorService.DeleteAuthor(local.Id, true);
|
|
}
|
|
|
|
protected override List<Book> GetRemoteChildren(Author local, Author remote)
|
|
{
|
|
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();
|
|
return all.Where(x => !excluded.Contains(x.ForeignBookId)).ToList();
|
|
}
|
|
|
|
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
|
|
{
|
|
return _bookService.GetBooksForRefresh(entity.AuthorMetadataId,
|
|
remoteChildren.Select(x => x.ForeignBookId).ToList());
|
|
}
|
|
|
|
protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote)
|
|
{
|
|
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignBookId == remote.ForeignBookId);
|
|
var mergeChildren = new List<Book>();
|
|
return Tuple.Create(existingChild, mergeChildren);
|
|
}
|
|
|
|
protected override void PrepareNewChild(Book child, Author entity)
|
|
{
|
|
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;
|
|
}
|
|
|
|
protected override void PrepareExistingChild(Book local, Book remote, Author entity)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
protected override void AddChildren(List<Book> children)
|
|
{
|
|
_bookService.InsertMany(children);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
protected override void PublishEntityUpdatedEvent(Author entity)
|
|
{
|
|
_eventAggregator.PublishEvent(new AuthorUpdatedEvent(entity));
|
|
}
|
|
|
|
protected override void PublishRefreshCompleteEvent(Author entity)
|
|
{
|
|
// 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));
|
|
}
|
|
|
|
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();
|
|
|
|
_commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, authorIds));
|
|
}
|
|
}
|
|
|
|
private void RefreshSelectedAuthors(List<int> authorIds, bool isNew, CommandTrigger trigger)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
Rescan(authorIds, isNew, trigger, updated);
|
|
}
|
|
}
|
|
}
|
|
}
|