Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,15 @@ public void should_mark_as_imported_if_the_download_can_be_tracked_using_the_sou
}

[Test]
public void should_pass_book_override_when_remotebook_has_single_book()
public void should_pass_book_override_when_remotebook_has_single_valid_local_book()
{
var book = CreateBook(42);
_trackedDownload.RemoteBook.Books = new List<Book> { book };

Mocker.GetMock<IBookService>()
.Setup(s => s.GetBook(42))
.Returns(book);

Mocker.GetMock<IDownloadedBooksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<IdentificationOverrides>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
Expand All @@ -388,6 +392,55 @@ public void should_pass_book_override_when_remotebook_has_single_book()
Times.Once());
}

[Test]
public void should_not_pass_book_override_when_single_book_has_zero_id()
{
var book = CreateBook(0);
_trackedDownload.RemoteBook.Books = new List<Book> { book };

Mocker.GetMock<IDownloadedBooksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<IdentificationOverrides>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>());

Subject.Import(_trackedDownload);

Mocker.GetMock<IDownloadedBooksImportService>()
.Verify(v => v.ProcessPath(
It.IsAny<string>(),
ImportMode.Auto,
It.Is<IdentificationOverrides>(o => o.Author == _trackedDownload.RemoteBook.Author && o.Book == null),
It.IsAny<DownloadClientItem>()),
Times.Once());

Mocker.GetMock<IBookService>()
.Verify(s => s.GetBook(It.IsAny<int>()), Times.Never());
}

[Test]
public void should_not_pass_book_override_when_single_book_not_found_in_db()
{
var book = CreateBook(99);
_trackedDownload.RemoteBook.Books = new List<Book> { book };

Mocker.GetMock<IBookService>()
.Setup(s => s.GetBook(99))
.Returns((Book)null);

Mocker.GetMock<IDownloadedBooksImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<IdentificationOverrides>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>());

Subject.Import(_trackedDownload);

Mocker.GetMock<IDownloadedBooksImportService>()
.Verify(v => v.ProcessPath(
It.IsAny<string>(),
ImportMode.Auto,
It.Is<IdentificationOverrides>(o => o.Author == _trackedDownload.RemoteBook.Author && o.Book == null),
It.IsAny<DownloadClientItem>()),
Times.Once());
}

[Test]
public void should_not_pass_book_override_when_remotebook_has_multiple_books()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.BookImport;
using NzbDrone.Core.MediaFiles.BookImport.Identification;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.Goodreads;
Expand All @@ -13,6 +18,70 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport.Identification
[TestFixture]
public class CandidateServiceFixture : CoreTest<CandidateService>
{
private LocalEdition BuildLocalEdition(string bookTitle = "Book", string author = "Author")
{
return new LocalEdition
{
LocalBooks = new List<LocalBook>
{
new LocalBook
{
FileTrackInfo = new ParsedTrackInfo
{
Authors = new List<string> { author },
BookTitle = bookTitle
}
}
}
};
}

[Test]
public void should_fall_back_to_author_search_when_book_override_returns_no_candidates()
{
var author = new Author { Id = 1, AuthorMetadataId = 10 };
var book = new Book { Id = 99 };
var idOverrides = new IdentificationOverrides { Author = author, Book = book };

// Book override path returns nothing (stale book)
Mocker.GetMock<IEditionService>()
.Setup(s => s.GetEditionsByBook(99))
.Returns(new List<Edition>());

// Author fallback should find candidates
var edition = new Edition
{
Id = 5,
BookId = 50,
ForeignEditionId = "edition-1",
Ratings = new Ratings { Votes = 1, Value = 1.0m },
Book = new LazyLoaded<Book>(new Book
{
Id = 50,
ForeignBookId = "foreign-50",
Author = new LazyLoaded<Author>(author)
})
};

Mocker.GetMock<IBookService>()
.Setup(s => s.GetCandidates(10, "Book"))
.Returns(new List<Book> { new Book { Id = 50 } });

Mocker.GetMock<IEditionService>()
.Setup(s => s.GetEditionsByBook(50))
.Returns(new List<Edition> { edition });

Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByBook(It.IsAny<int>()))
.Returns(new List<BookFile>());

var localEdition = BuildLocalEdition();
var result = Subject.GetDbCandidatesFromTags(localEdition, idOverrides, false);

result.Should().HaveCount(1);
result.First().Edition.Id.Should().Be(5);
}

[Test]
public void should_not_throw_on_goodreads_exception()
{
Expand Down
16 changes: 13 additions & 3 deletions src/NzbDrone.Core/Download/CompletedDownloadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.Books;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.BookImport;
using NzbDrone.Core.MediaFiles.Events;
Expand All @@ -28,20 +29,23 @@ public class CompletedDownloadService : ICompletedDownloadService
private readonly IProvideImportItemService _provideImportItemService;
private readonly IDownloadedBooksImportService _downloadedTracksImportService;
private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported;
private readonly IBookService _bookService;
private readonly Logger _logger;

public CompletedDownloadService(IEventAggregator eventAggregator,
IHistoryService historyService,
IProvideImportItemService provideImportItemService,
IDownloadedBooksImportService downloadedTracksImportService,
ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported,
IBookService bookService,
Logger logger)
{
_eventAggregator = eventAggregator;
_historyService = historyService;
_provideImportItemService = provideImportItemService;
_downloadedTracksImportService = downloadedTracksImportService;
_trackedDownloadAlreadyImported = trackedDownloadAlreadyImported;
_bookService = bookService;
_logger = logger;
}

Expand Down Expand Up @@ -203,11 +207,17 @@ private IdentificationOverrides BuildIdentificationOverrides(Parser.Model.Remote
Author = remoteBook.Author
};

// Only set Book override when the grab targeted exactly one book,
// so we don't incorrectly force multi-book/omnibus downloads to a single book.
// Only set Book override when the grab targeted exactly one book
// AND we can confirm the book exists in the local database.
// RemoteBook.Books may contain stale or unmapped objects that would
// cause CandidateService.GetDbCandidatesByBook to return zero candidates.
if (remoteBook.Books != null && remoteBook.Books.Count == 1)
{
overrides.Book = remoteBook.Books.First();
var candidate = remoteBook.Books.First();
if (candidate.Id > 0 && _bookService.GetBook(candidate.Id) != null)
{
overrides.Book = candidate;
}
}

return overrides;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ public List<CandidateEdition> GetDbCandidatesFromTags(LocalEdition localEdition,
{
candidateReleases = GetDbCandidatesByBook(idOverrides.Book, includeExisting);
}

// If the book override produced no candidates (e.g. stale/invalid book),
// fall back to author-scoped search so import can still proceed.
if (!candidateReleases.Any() && idOverrides.Author != null)
{
_logger.Debug("Book override returned no candidates, falling back to author search");
candidateReleases = GetDbCandidatesByAuthor(localEdition, idOverrides.Author, includeExisting);
}
}
else if (idOverrides?.Author != null)
{
Expand Down
10 changes: 9 additions & 1 deletion src/NzbDrone.Core/MediaFiles/DownloadedBooksCommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.MediaFiles.BookImport;
Expand All @@ -20,20 +21,23 @@ public class DownloadedBooksCommandService : IExecute<DownloadedBooksScanCommand
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDiskProvider _diskProvider;
private readonly ICompletedDownloadService _completedDownloadService;
private readonly IBookService _bookService;
private readonly ICommandResultReporter _commandResultReporter;
private readonly Logger _logger;

public DownloadedBooksCommandService(IDownloadedBooksImportService downloadedTracksImportService,
ITrackedDownloadService trackedDownloadService,
IDiskProvider diskProvider,
ICompletedDownloadService completedDownloadService,
IBookService bookService,
ICommandResultReporter commandResultReporter,
Logger logger)
{
_downloadedTracksImportService = downloadedTracksImportService;
_trackedDownloadService = trackedDownloadService;
_diskProvider = diskProvider;
_completedDownloadService = completedDownloadService;
_bookService = bookService;
_commandResultReporter = commandResultReporter;
_logger = logger;
}
Expand Down Expand Up @@ -82,7 +86,11 @@ private IdentificationOverrides BuildIdentificationOverrides(RemoteBook remoteBo

if (remoteBook.Books != null && remoteBook.Books.Count == 1)
{
overrides.Book = remoteBook.Books.First();
var candidate = remoteBook.Books.First();
if (candidate.Id > 0 && _bookService.GetBook(candidate.Id) != null)
{
overrides.Book = candidate;
}
}

return overrides;
Expand Down
Loading