Skip to content
Open
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 @@ -677,5 +677,88 @@ private static object InvokeV5SuggestionWithPathFallback(
questionsWithoutSuggestion ?? new HashSet<string>(StringComparer.Ordinal)
});
}

[Test]
public async Task author_restricted_mode_should_link_v5_suggested_author_that_already_exists_when_import_disallowed()
{
var logger = LogManager.GetCurrentClassLogger();
var containment = new ContainmentValidator(new TagNormalizer(), logger);
var v5 = new RecordingV5MatchingService
{
OnSearch = (_, tags, _, _) =>
{
var artistValues = tags.TryGetValue("ARTIST", out var artist) ? artist : new List<string>();
if (artistValues.Any(v => string.Equals(v, "Frank Herbert", StringComparison.OrdinalIgnoreCase)))
{
return new List<V5MatchedAuthor>
{
new V5MatchedAuthor
{
id = "hc:frank-herbert",
name = "Frank Herbert",
edition_hardcover_id = "hc-ed-675"
}
};
}

return new List<V5MatchedAuthor>();
}
};
var fts = new RecordingEditionFtsRepository();
var authorService = new StubAuthorService(
(new Author { Id = 31, Name = "Brian Herbert", Path = "/audiobooks/Brian Herbert" }, "hc", "brian-herbert"),
(new Author { Id = 6, Name = "Frank Herbert", Path = "/audiobooks/Frank Herbert" }, "hc", "frank-herbert"));

var svc = new FileMatchingService(
matchingLogger: new NullMatchingUploadLogger(),
v5MatchingService: v5,
containmentValidator: containment,
pendingAuthorImportService: null,
commandQueue: null,
authorFolderMatchingService: null,
rootFolderService: null,
configService: ConfigServiceTestProxy.Create(strictness: BookMatchingStrictness.Balanced, usePathAsTagsFallback: true),
authorService: authorService,
eventAggregator: null,
authorLibraryService: null,
editionFtsRepository: fts,
bookService: null,
editionService: null,
editionRepository: null,
mediaInfoExtractor: null,
logger: logger);

var file = new DiscoveredFileWithMetadata
{
Path = "/audiobooks/Brian Herbert/Whipping Star/Whipping Star.m4b",
DurationSeconds = 36010,
AllTags = new Dictionary<string, List<string>>
{
{ "TITLE", new List<string> { "Whipping Star" } },
{ "ARTIST", new List<string> { "Frank Herbert" } }
}
};

// Downloaded-import style context: V5 identification on, author
// import off. The suggested author already exists in the library,
// so the file must still link to it.
var context = new MatchingContext
{
AllowV5Identification = true,
AllowAuthorImport = false,
DeferUnmatchedToAuthorReady = false,
AllowUnscopedFallback = false,
DisablePathFallback = true,
PerFileMatching = false
};

var result = await svc.MatchFilesToLibraryAsync(new[] { file }, restrictToAuthorId: 31, context);

Assert.That(result.UnmatchedFiles, Is.Empty);
Assert.That(result.MatchedFiles, Has.Length.EqualTo(1));
Assert.That(result.MatchedFiles[0].AuthorId, Is.EqualTo(6));
Assert.That(result.MatchedFiles[0].BookTitle, Is.EqualTo("Whipping Star"));
}

}
}
50 changes: 49 additions & 1 deletion src/NzbDrone.Core/MediaFiles/BookImport/FileMatchingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3146,7 +3146,31 @@ private bool TryResolveLocalV5WorkBoundary(

if (!allowAuthorImport)
{
return (null, suggestion, s.Reason);
// Import is off-limits here, but linking to an author the
// library already has is not an import. Without this lookup
// every V5 suggestion whose author entered the library under
// a different provider id dies as "no match in local library"
// even though the author (and often the book) is present.
var existingOnly = TryFindExistingSuggestedAuthor(s.ProviderId);
if (existingOnly == null || existingOnly.Id <= 0)
{
return (null, suggestion, s.Reason);
}

var scopedExistingMatch = EvaluateHolyGrailMatchFileInternal(
file,
mediaType,
existingOnly.Id,
disablePathFallback: true,
inferAuthorFromPathDuringPathFallback: true,
unscoped: false,
hardAllowedBookIds: hardAllowedBookIds)?.Match;
if (scopedExistingMatch != null)
{
return (scopedExistingMatch, suggestion, $"Recovered via existing V5 author '{s.AuthorName}'");
}

return (null, suggestion, $"NO_EDITION_FOUND (existing authorId={existingOnly.Id})");
}

var recoveredAuthor = TryGetOrImportSuggestedAuthorForRestrictedRecovery(s.ProviderId, s.AuthorName, file.Path, mediaType);
Expand All @@ -3171,6 +3195,30 @@ private bool TryResolveLocalV5WorkBoundary(
return (null, suggestion, $"NO_EDITION_FOUND (authorId={recoveredAuthor.Id})");
}

private Author TryFindExistingSuggestedAuthor(string providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
{
return null;
}

var colon = providerId.IndexOf(':');
if (colon <= 0 || colon >= providerId.Length - 1)
{
return null;
}

try
{
return _authorService?.FindByProviderId(providerId.Substring(0, colon), providerId.Substring(colon + 1));
}
catch (Exception ex)
{
_logger.Debug(ex, "[FALLBACK-V5] Existing-author lookup failed for '{0}'", providerId);
return null;
}
}

private Author TryGetOrImportSuggestedAuthorForRestrictedRecovery(string providerId, string authorName, string samplePath, BookMediaType mediaType)
{
if (string.IsNullOrWhiteSpace(providerId))
Expand Down
Loading