feat(books): add AudiobookCovers.com as a cover search source#565
Conversation
AudiobookCovers.com hosts community-submitted audiobook cover art and has no public API, so results are scraped from its server-rendered search page. Since it only indexes square audiobook fan covers (no title/author metadata), the provider is scoped to audiobook searches and surfaced in the cover picker only when the audiobook toggle is on. Closes: bookorbit#564 Co-Authored-By: Claude Sonnet 5 <[email protected]>
📝 WalkthroughWalkthroughAdds a new AudiobookCoversCoverProvider that scrapes audiobookcovers.com for audiobook cover images, registers it in the server's CoverModule and provider key type, and updates the client CoverSearchDrawer to conditionally display and reset the audiobookcovers source option based on audiobook mode. ChangesAudiobookCovers source support
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CoverModule
participant AudiobookCoversCoverProvider
participant AudiobookCoversSite
CoverModule->>AudiobookCoversCoverProvider: search(params)
AudiobookCoversCoverProvider->>AudiobookCoversCoverProvider: buildQuery(title, author)
alt not audiobook or empty query
AudiobookCoversCoverProvider-->>CoverModule: []
else valid query
AudiobookCoversCoverProvider->>AudiobookCoversSite: fetch(searchUrl, headers, timeout)
alt fetch succeeds (200 OK)
AudiobookCoversSite-->>AudiobookCoversCoverProvider: HTML response
AudiobookCoversCoverProvider->>AudiobookCoversCoverProvider: parseResults(html), dedupe by id
AudiobookCoversCoverProvider-->>CoverModule: CoverSearchResult[] (max 25)
else fetch fails or non-OK status
AudiobookCoversCoverProvider->>AudiobookCoversCoverProvider: log warning
AudiobookCoversCoverProvider-->>CoverModule: []
end
end
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
client/src/features/book/components/detail/tabs/CoverSearchDrawer.vue (1)
29-46: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
searchProvidernot reset when drawer reopens with a different audiobook state.
toggleAudiobookSearch(Lines 44-46) resetssearchProvideraway from'audiobookcovers'when the checkbox is toggled off, but thewatch(() => props.open, ...)handler (Lines 29-40) — which re-syncsisAudiobookSearchfromprops.isAudiobookwhenever the drawer reopens for a new book — does not perform the same reset.If a user selects "AudiobookCovers" for one book, closes the drawer, then reopens it for a non-audiobook,
isAudiobookSearchbecomesfalse(hiding the option) whilesearchProvidersilently remains'audiobookcovers'. The select loses a valid displayed selection, and searching would sendprovider=audiobookcoverswithisAudiobook=false, which the server-side provider always rejects — producing an unexplained empty result set.🐛 Proposed fix
watch( () => props.open, (val) => { if (val) { searchTitle.value = props.initialTitle searchAuthor.value = props.initialAuthor isAudiobookSearch.value = props.isAudiobook searchResults.value = [] hasSearched.value = false + if (!isAudiobookSearch.value && searchProvider.value === 'audiobookcovers') { + searchProvider.value = 'duckduckgo' + } } }, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/features/book/components/detail/tabs/CoverSearchDrawer.vue` around lines 29 - 46, `searchProvider` is not being reset when `CoverSearchDrawer` reopens with a different audiobook state. Update the `watch(() => props.open, ...)` handler to keep `searchProvider` consistent with `props.isAudiobook`, the same way `toggleAudiobookSearch()` does when turning audiobook search off. When `props.isAudiobook` is false and the current provider is `'audiobookcovers'`, reset it to a valid non-audiobook provider such as `'duckduckgo'` so reopening the drawer cannot leave an invalid hidden selection.
🧹 Nitpick comments (1)
server/src/modules/cover/providers/audiobookcovers-cover-provider.ts (1)
10-10: 🧹 Nitpick | 🔵 TrivialScraping approach is inherently fragile to markup changes.
The regex bridges from
id:"..."to the nearest serializedjpeg:$R[n]={...}block, coupling parsing tightly to audiobookcovers.com's current server-rendered output. If the site changes its markup/serialization,parseResultswill silently return an empty array with no distinguishing signal from a legitimate "no results" case, making regressions hard to detect.Consider logging when a response is fetched successfully (200 OK) but zero results are parsed, to help distinguish "no results" from "scraper broke."
[reliability]
Also applies to: 52-71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/cover/providers/audiobookcovers-cover-provider.ts` at line 10, The parsing in parseResults is fragile and can fail silently when audiobookcovers.com changes its serialized markup. Update the result handling around RESULT_PATTERN and parseResults so that a successful 200 response with zero parsed matches emits a clear log/warning, distinguishing an actual no-results case from a scraper break. Use the existing provider flow in audiobookcovers-cover-provider to detect the fetched response status and log when parsing returns an empty array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/src/features/book/components/detail/tabs/CoverSearchDrawer.test.ts`:
- Around line 133-156: Add a test covering the open-prop reset path in
CoverSearchDrawer.test.ts: the current cases only verify the checkbox-driven
reset, so extend the existing CoverSearchDrawer/mountDrawer setup to simulate
closing and reopening the drawer via props.open while switching isAudiobook from
true to false. After selecting audiobookcovers, assert that the watch on
props.open causes searchProvider to reset so the select is no longer
audiobookcovers (for example, it falls back to duckduckgo).
---
Outside diff comments:
In `@client/src/features/book/components/detail/tabs/CoverSearchDrawer.vue`:
- Around line 29-46: `searchProvider` is not being reset when
`CoverSearchDrawer` reopens with a different audiobook state. Update the
`watch(() => props.open, ...)` handler to keep `searchProvider` consistent with
`props.isAudiobook`, the same way `toggleAudiobookSearch()` does when turning
audiobook search off. When `props.isAudiobook` is false and the current provider
is `'audiobookcovers'`, reset it to a valid non-audiobook provider such as
`'duckduckgo'` so reopening the drawer cannot leave an invalid hidden selection.
---
Nitpick comments:
In `@server/src/modules/cover/providers/audiobookcovers-cover-provider.ts`:
- Line 10: The parsing in parseResults is fragile and can fail silently when
audiobookcovers.com changes its serialized markup. Update the result handling
around RESULT_PATTERN and parseResults so that a successful 200 response with
zero parsed matches emits a clear log/warning, distinguishing an actual
no-results case from a scraper break. Use the existing provider flow in
audiobookcovers-cover-provider to detect the fetched response status and log
when parsing returns an empty array.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f67ce432-73fa-47c9-9f75-00b352ea2ee4
📒 Files selected for processing (8)
client/src/features/book/components/detail/tabs/CoverSearchDrawer.test.tsclient/src/features/book/components/detail/tabs/CoverSearchDrawer.vueserver/src/modules/cover/cover.module.test.tsserver/src/modules/cover/cover.module.tsserver/src/modules/cover/dto/search-covers-query.dto.test.tsserver/src/modules/cover/providers/audiobookcovers-cover-provider.test.tsserver/src/modules/cover/providers/audiobookcovers-cover-provider.tsserver/src/modules/cover/providers/cover-provider.ts
| describe('audiobookcovers source option', () => { | ||
| it('is hidden from the source dropdown for regular books', () => { | ||
| const wrapper = mountDrawer({ isAudiobook: false }) | ||
| const options = wrapper.findAll('option').map((option) => option.element.value) | ||
| expect(options).not.toContain('audiobookcovers') | ||
| }) | ||
|
|
||
| it('is shown in the source dropdown for audiobooks', () => { | ||
| const wrapper = mountDrawer({ isAudiobook: true }) | ||
| const options = wrapper.findAll('option').map((option) => option.element.value) | ||
| expect(options).toContain('audiobookcovers') | ||
| }) | ||
|
|
||
| it('resets the selected provider away from audiobookcovers when audiobook is unchecked', async () => { | ||
| const wrapper = mountDrawer({ isAudiobook: true }) | ||
| await wrapper.find('select').setValue('audiobookcovers') | ||
|
|
||
| await wrapper.find('input[type="checkbox"]').trigger('change') | ||
|
|
||
| const select = wrapper.find('select').element as HTMLSelectElement | ||
| expect(select.value).toBe('duckduckgo') | ||
| }) | ||
| }) | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add a test for the open-prop-triggered reset path.
Current tests only exercise the checkbox-triggered reset (Lines 146-154). Given the state-desync bug flagged in CoverSearchDrawer.vue (watch on props.open doesn't reset searchProvider), consider adding a test that mounts with isAudiobook: true, selects audiobookcovers, sets open to false then back to true with isAudiobook: false, and asserts the select value is no longer audiobookcovers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/src/features/book/components/detail/tabs/CoverSearchDrawer.test.ts`
around lines 133 - 156, Add a test covering the open-prop reset path in
CoverSearchDrawer.test.ts: the current cases only verify the checkbox-driven
reset, so extend the existing CoverSearchDrawer/mountDrawer setup to simulate
closing and reopening the drawer via props.open while switching isAudiobook from
true to false. After selecting audiobookcovers, assert that the watch on
props.open causes searchProvider to reset so the select is no longer
audiobookcovers (for example, it falls back to duckduckgo).
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
🎉 This PR is included in version 2.2.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Before you submit
This issue (#564) was opened by me and doesn't yet have explicit maintainer approval (no 👍 or "go ahead" comment). Opening this PR anyway per my own judgment call — happy to convert to draft or hold if maintainers would rather discuss first.
What does this PR do?
Adds AudiobookCovers.com as a cover search source in the metadata editor's cover picker, as requested in the issue.
AudiobookCovers.com has no public API — it renders search results server-side and embeds them as a serialized object graph in the page HTML. The new
AudiobookCoversCoverProviderfetches/search?q=<title author>and extracts the image records (id + jpeg URLs at 320/640/1280px) via a targeted regex, following the same pattern as the existingDuckDuckGoCoverProvider/ITunesCoverProviderscrapers.The site only contains community-submitted square audiobook fan art (no title/author metadata — results are ranked by an embedding-similarity
distancefield, not text matching), so the provider:isAudiobookis setNo config/settings changes — it's always active for audiobook searches, same as DuckDuckGo (no enable/disable toggle exists for that provider either).
Closes: #564
How did you test this?
/search?q=responses withcurland validated the extraction regex against them in a standalone script (correctly extracted all 31/31 embedded results, in relevance order).CoverSearchDrawertests for the new provider key/UI option.pnpm test/lint locally — my working copy is on an exFAT-formatted mount, which doesn't support the symlinks pnpm needs, sopnpm installcouldn't complete in this environment. I read through the diff carefully and hand-verified the regex against real page data instead, but the actual test suite has not been executed against this code. Flagging this explicitly rather than checking the box below — happy to fix anything CI turns up.Screenshots
Not included — this is primarily a backend addition plus one new
<option>in an existing dropdown; I didn't have a working local dev server to capture the UI in this session (same environment issue as above).Anything non-obvious in the diff?
$R[...]object-graph format) rather than a documented API, since none exists. This is inherently a little more fragile than a JSON API integration — if AudiobookCovers.com changes its frontend internals, the regex may need updating. It scrapes onlyidand the threejpegsize URLs, ignoring the surrounding blurhash/color/distance fields.width/heightare hardcoded to 1280 rather than derived from any field in the payload (the site doesn't expose dimensions).Checklist
.env, personal configs)AI tools used: Claude Code
Extent: Used to research the target site's structure (browser inspection + curl), write the provider implementation, tests, and this PR description. I reviewed the full diff and the site-scraping logic myself before submitting.
Summary by CodeRabbit
New Features
Bug Fixes