feat(metadata): fallback to search if lookup by id fails

Update the metadata fetch service to perform a fallback search when a provider's lookup by ID returns no result or an irrelevant candidate. This ensures that metadata retrieval can gracefully recover from incorrect or stale provider IDs.

Closes #182
This commit is contained in:
neon
2026-06-01 05:38:43 +00:00
parent 9111f84c47
commit af8a855b3b
2 changed files with 38 additions and 4 deletions
@@ -186,7 +186,7 @@ describe('MetadataFetchService', () => {
expect(google.search).not.toHaveBeenCalled();
});
it('returns no result when lookupById returns null for an existing provider id', async () => {
it('falls back to provider search when lookupById returns null for an existing provider id', async () => {
const google: IdentifiableProvider = {
key: MetadataProviderKey.GOOGLE,
label: 'Google',
@@ -200,9 +200,36 @@ describe('MetadataFetchService', () => {
service.search({ title: 'Dune', existingProviderIds: { [MetadataProviderKey.GOOGLE]: 'missing' } }).pipe(toArray()),
);
expect(results).toEqual([]);
expect(results).toEqual([candidate(MetadataProviderKey.GOOGLE, 'search-id', 'Dune')]);
expect(google.lookupById).toHaveBeenCalledWith('missing', expect.anything());
expect(google.search).not.toHaveBeenCalled();
expect(google.search).toHaveBeenCalledTimes(1);
expect(google.search).toHaveBeenCalledWith(expect.objectContaining({ title: 'Dune' }));
});
it('falls back to provider search when lookupById returns an irrelevant candidate', async () => {
const google: IdentifiableProvider = {
key: MetadataProviderKey.GOOGLE,
label: 'Google',
identifiable: true,
search: vi.fn().mockResolvedValue([candidate(MetadataProviderKey.GOOGLE, 'search-id', 'Dune')]),
lookupById: vi.fn().mockResolvedValue(candidate(MetadataProviderKey.GOOGLE, 'stored-id', 'Completely Unrelated')),
};
registry.select.mockReturnValue([google]);
const results = await firstValueFrom(
service
.search({
title: 'Dune',
author: 'Frank Herbert',
existingProviderIds: { [MetadataProviderKey.GOOGLE]: 'stored-id' },
})
.pipe(toArray()),
);
expect(results).toEqual([candidate(MetadataProviderKey.GOOGLE, 'search-id', 'Dune')]);
expect(google.lookupById).toHaveBeenCalledWith('stored-id', expect.anything());
expect(google.search).toHaveBeenCalledTimes(1);
expect(google.search).toHaveBeenCalledWith(expect.objectContaining({ title: 'Dune', author: 'Frank Herbert' }));
});
it('isolates provider failures so one provider error does not fail the full stream', async () => {
@@ -114,9 +114,16 @@ export class MetadataFetchService {
const existingProviderId = params.existingProviderIds?.[provider.key];
if (isIdentifiable(provider) && existingProviderId) {
const lookupResult = await provider.lookupById(existingProviderId, params.signal);
return lookupResult ? [lookupResult] : [];
if (lookupResult) {
const rankedLookup = filterAndRank([lookupResult], params, 1);
if (rankedLookup.length > 0) return rankedLookup;
}
}
return this.searchAndRankProvider(provider, params);
}
private async searchAndRankProvider(provider: MetadataProvider, params: MetadataSearchParams): Promise<MetadataCandidate[]> {
const primary = filterAndRank(await provider.search(params), params);
const hasIsbn = hasText(params.isbn);
if (!hasIsbn) return primary;