feat(metadata): add LubimyCzytac metadata provider#324
Conversation
- Add LubimyCzytac scraper and provider implementation - Add lubimyczytacId field across types, schema, extractors, and metadata service - Register lubimyczytac provider in module, pipeline, repository, and config service - Add provider icon, color, link, and UI support in book detail views - Add migration for lubimyczytac_id column Closes bookorbit#304
📝 WalkthroughWalkthroughAdds LubimyCzytac provider support across types, DB migration and schema, server scraper/provider implementation and tests, extractor/file-write plumbing, metadata persistence and lock handling, client UI/provider links and editor fields, module wiring, and test fixtures; plus a BookCoverCard overlay UI tweak. ChangesLubimyCzytac Provider Integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/lib/provider-links.ts`:
- Around line 1-7: The lubimyczytacBookUrl function must validate the trimmed
id: after const value = String(id).trim() check that value is non-empty and does
not start or end with a slash (and is not just "/"); if validation fails return
a safe fallback (e.g., empty string or null) or throw an error per project
convention; otherwise normalize by removing any leading slashes and then build
the path exactly as before (use existing logic: if no slash add "/-" else keep
canonical id/slug and encodeURIComponent each segment).
In
`@server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.ts`:
- Around line 52-56: The loop that fetches each detail (the for (const link of
links.slice(0, maxCandidates)) loop) currently calls sleep only after a prior
result is present; move the pacing call to the start of each iteration so the
configured gap (PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS) is applied
before the first detail fetch too: after checking params.signal?.aborted at the
top of the loop, call await
sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal) and
remove the results.length > 0 condition that guarded the existing sleep call so
the delay is applied unconditionally for each detail request.
🪄 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: a90a2a71-6f23-44d1-8407-fafbb364aeaa
⛔ Files ignored due to path filters (1)
client/public/assets/provider-icons/lubimyczytac.svgis excluded by!**/*.svg
📒 Files selected for processing (64)
client/src/features/book/components/BookQuickView.vueclient/src/features/book/components/detail/tabs/DetailsTab.vueclient/src/features/book/components/detail/tabs/EditMetadataTab.vueclient/src/features/book/composables/useFileMetadata.tsclient/src/features/book/composables/useMetadataDiff.tsclient/src/features/book/composables/useMetadataEditor.tsclient/src/features/book/lib/file-metadata-patch.tsclient/src/features/book/lib/metadata-refresh-feedback.tsclient/src/features/book/lib/provider-icons.tsclient/src/features/book/lib/provider-links.tsclient/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vueclient/src/lib/provider-colors.tspackages/types/src/book.tspackages/types/src/file-write.tspackages/types/src/metadata-fetch.tspackages/types/src/metadata-lock.tspackages/types/src/metadata-preferences.tsserver/src/db/migrations/0019_add_lubimyczytac_provider_id.sqlserver/src/db/migrations/meta/0019_snapshot.jsonserver/src/db/migrations/meta/_journal.jsonserver/src/db/schema/metadata.tsserver/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.tsserver/src/modules/book-metadata-lock/book-metadata-lock.service.tsserver/src/modules/book/book.service.tsserver/src/modules/book/dto/update-book-metadata.dto.tsserver/src/modules/cover/providers/itunes-cover-provider.test.tsserver/src/modules/file-write/file-write.constants.tsserver/src/modules/file-write/file-write.repository.tsserver/src/modules/file-write/interfaces/book-write-payload.interface.tsserver/src/modules/metadata-fetch/metadata-fetch-pipeline.test.tsserver/src/modules/metadata-fetch/metadata-fetch.controller.test.tsserver/src/modules/metadata-fetch/metadata-fetch.module.test.tsserver/src/modules/metadata-fetch/metadata-fetch.module.tsserver/src/modules/metadata-fetch/metadata-fetch.repository.tsserver/src/modules/metadata-fetch/metadata-fetch.service.tsserver/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.tsserver/src/modules/metadata-fetch/providers/audible/audible.provider.test.tsserver/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.tsserver/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.tsserver/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.tsserver/src/modules/metadata-fetch/providers/google/google.provider.test.tsserver/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.tsserver/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.tsserver/src/modules/metadata-fetch/providers/kobo/kobo.provider.test.tsserver/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.test.tsserver/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.tsserver/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.test.tsserver/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.tsserver/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.tsserver/src/modules/metadata-fetch/providers/provider-constants.tsserver/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.tsserver/src/modules/metadata-preferences/dto/update-provider-config.dto.tsserver/src/modules/metadata-preferences/provider-config.service.test.tsserver/src/modules/metadata-preferences/provider-config.service.tsserver/src/modules/metadata/extractors/comic-format.extractor.tsserver/src/modules/metadata/extractors/format-extractor.interface.tsserver/src/modules/metadata/extractors/opf-metadata.mapper.tsserver/src/modules/metadata/extractors/pdf-format.extractor.tsserver/src/modules/metadata/lib/cbz-metadata.tsserver/src/modules/metadata/lib/opf-parser.tsserver/src/modules/metadata/lib/pdf-parser.tsserver/src/modules/metadata/lib/pdf-xmp-reader.tsserver/src/modules/metadata/metadata.service.tsserver/src/modules/statistics/statistics.repository.ts
| export function lubimyczytacBookUrl(id: string): string { | ||
| const value = String(id).trim() | ||
| // New data stores the canonical "id/slug" path. lubimyczytac.pl 404s on a slug-less id, | ||
| // so legacy bare-numeric ids get a placeholder slug segment to keep the link valid. | ||
| const path = value.includes('/') ? value : `${value}/-` | ||
| return `https://lubimyczytac.pl/ksiazka/${path.split('/').map(encodeURIComponent).join('/')}` | ||
| } |
There was a problem hiding this comment.
Add input validation for edge-case strings.
The function does not validate that id is non-empty after trimming or that slash patterns are well-formed. Edge cases that could produce malformed URLs:
- Whitespace-only input: A string like
" "is truthy in the caller's check (if (ids.lubimyczytac)), but aftertrim()becomes"", resulting inpath = "/-"and a double-slash URL:https://lubimyczytac.pl/ksiazka//-. - Malformed slash inputs: Inputs like
"/"(slash only) or"/123"(leading slash) would produce invalid double-slash URLs.
The components perform truthy checks before calling this function, which prevents null, undefined, and empty strings, but whitespace-only strings bypass that check.
🛡️ Suggested input validation
export function lubimyczytacBookUrl(id: string): string {
const value = String(id).trim()
+ if (!value) {
+ throw new Error('lubimyczytac ID cannot be empty')
+ }
// New data stores the canonical "id/slug" path. lubimyczytac.pl 404s on a slug-less id,
// so legacy bare-numeric ids get a placeholder slug segment to keep the link valid.
const path = value.includes('/') ? value : `${value}/-`
return `https://lubimyczytac.pl/ksiazka/${path.split('/').map(encodeURIComponent).join('/')}`
}Alternatively, if throwing is undesirable, return a safe fallback or validate the format more strictly (e.g., reject leading/trailing slashes).
🤖 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/lib/provider-links.ts` around lines 1 - 7, The
lubimyczytacBookUrl function must validate the trimmed id: after const value =
String(id).trim() check that value is non-empty and does not start or end with a
slash (and is not just "/"); if validation fails return a safe fallback (e.g.,
empty string or null) or throw an error per project convention; otherwise
normalize by removing any leading slashes and then build the path exactly as
before (use existing logic: if no slash add "/-" else keep canonical id/slug and
encodeURIComponent each segment).
| for (const link of links.slice(0, maxCandidates)) { | ||
| if (params.signal?.aborted) break; | ||
| if (results.length > 0) { | ||
| await sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal); | ||
| } |
There was a problem hiding this comment.
Apply the configured request gap before the first detail fetch.
On Line 54, the delay only runs after at least one result is already added, so the search request and first book-page request run back-to-back. That bypasses the configured between-request pacing and raises throttling risk.
💡 Proposed fix
for (const link of links.slice(0, maxCandidates)) {
if (params.signal?.aborted) break;
- if (results.length > 0) {
- await sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal);
- }
+ // Keep a gap between the search request and every detail-page request.
+ await sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal);
const candidate = await this.fetchByUrl(link.url, link.providerId, params.signal);
if (candidate) results.push(candidate);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (const link of links.slice(0, maxCandidates)) { | |
| if (params.signal?.aborted) break; | |
| if (results.length > 0) { | |
| await sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal); | |
| } | |
| for (const link of links.slice(0, maxCandidates)) { | |
| if (params.signal?.aborted) break; | |
| // Keep a gap between the search request and every detail-page request. | |
| await sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal); | |
| const candidate = await this.fetchByUrl(link.url, link.providerId, params.signal); | |
| if (candidate) results.push(candidate); | |
| } |
🤖 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/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.ts`
around lines 52 - 56, The loop that fetches each detail (the for (const link of
links.slice(0, maxCandidates)) loop) currently calls sleep only after a prior
result is present; move the pacing call to the start of each iteration so the
configured gap (PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS) is applied
before the first detail fetch too: after checking params.signal?.aborted at the
top of the loop, call await
sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal) and
remove the results.length > 0 condition that guarded the existing sleep call so
the delay is applied unconditionally for each detail request.
… click prefers details The overlay primary button should always open the reader regardless of the thumbnailClickAction setting. That setting only controls card click behaviour.
…click prefers details When thumbnailClickAction is 'details', a dedicated Read button now appears in the overlay top row so users can still open the reader directly without changing their preference.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
client/src/features/book/components/BookCoverCard.vue (2)
530-536: ⚡ Quick winConsider adding an accessible label to the explicit Read button.
The new explicit Read button is icon-only and lacks an
aria-labelattribute. Screen reader users may not understand the button's purpose. Consider addingaria-label="Read book"or similar descriptive text.Note: The adjacent Quick View button (lines 527-529) also lacks an
aria-label, so this may be a broader pattern in the component worth addressing.♿ Proposed accessibility improvement
<button v-if="showExplicitReadButton" class="p-[3cqi] rounded-[2.5cqi] bg-black/50 hover:bg-black/30 transition-colors text-white" + aria-label="Read book" `@click.stop`="openPrimaryFileExplicit" > <BookOpen class="size-[12cqi]" /> </button>🤖 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/BookCoverCard.vue` around lines 530 - 536, The explicit Read button in the BookCoverCard.vue component is icon-only and missing an accessibility label; update the button element that calls openPrimaryFileExplicit to include a descriptive aria-label (for example aria-label="Read book (explicit content)" or similar), and likewise add an appropriate aria-label to the adjacent Quick View button (the icon-only button above that opens the quick view) so both icon-only controls are announced to screen readers; ensure the labels are concise and contextually distinguish the two actions.
257-258: ⚡ Quick winConsolidate duplicate computed logic.
isPrimaryClickAvailable(line 257) andshowPrimaryOverlayAction(line 258) have identical implementations. Consider removing the duplication by having one reference the other or using a single computed property for both purposes.♻️ Proposed consolidation
Option 1: Reuse
showPrimaryOverlayActiondirectly-const isPrimaryClickAvailable = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value)) -const showPrimaryOverlayAction = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value)) +const showPrimaryOverlayAction = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value))Then on line 388, replace
isPrimaryClickAvailablewithshowPrimaryOverlayAction.Option 2: Have one reference the other
const isPrimaryClickAvailable = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value)) -const showPrimaryOverlayAction = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value)) +const showPrimaryOverlayAction = computed(() => isPrimaryClickAvailable.value)🤖 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/BookCoverCard.vue` around lines 257 - 258, The two computed properties isPrimaryClickAvailable and showPrimaryOverlayAction in BookCoverCard.vue contain identical logic; remove the duplication by keeping a single computed (e.g., showPrimaryOverlayAction = computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value != null && !isMissing.value))) and make isPrimaryClickAvailable a simple alias referencing that computed (or remove isPrimaryClickAvailable and replace its usages with showPrimaryOverlayAction); update all usages (e.g., the reference noted near line 388) to use the remaining computed so only one source of truth remains.
🤖 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.
Nitpick comments:
In `@client/src/features/book/components/BookCoverCard.vue`:
- Around line 530-536: The explicit Read button in the BookCoverCard.vue
component is icon-only and missing an accessibility label; update the button
element that calls openPrimaryFileExplicit to include a descriptive aria-label
(for example aria-label="Read book (explicit content)" or similar), and likewise
add an appropriate aria-label to the adjacent Quick View button (the icon-only
button above that opens the quick view) so both icon-only controls are announced
to screen readers; ensure the labels are concise and contextually distinguish
the two actions.
- Around line 257-258: The two computed properties isPrimaryClickAvailable and
showPrimaryOverlayAction in BookCoverCard.vue contain identical logic; remove
the duplication by keeping a single computed (e.g., showPrimaryOverlayAction =
computed(() => thumbnailClickAction.value === 'details' || (primaryFile.value !=
null && !isMissing.value))) and make isPrimaryClickAvailable a simple alias
referencing that computed (or remove isPrimaryClickAvailable and replace its
usages with showPrimaryOverlayAction); update all usages (e.g., the reference
noted near line 388) to use the remaining computed so only one source of truth
remains.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e1e296fe-1b3f-41aa-b9b3-4c9c74bb6212
📒 Files selected for processing (1)
client/src/features/book/components/BookCoverCard.vue
|
🎉 This PR is included in version 1.11.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
@chrismansell26 is that working for you? I just created issue recently regarding this feature #469 |
Adds lubimyczytac.pl as a metadata source, a Polish book/audiobook catalog with no public API, so the provider scrapes public pages.
LubimyCzytacScraperandLubimyCzytacProvider(search + lookupById)lubimyczytacIdfield added to types, DB schema, file-write fields, lock groups, extractors (PDF, CBZ, OPF), and metadata service0019_add_lubimyczytac_provider_idCloses #304
How did you test this?
lubimyczytac.scraper.test.ts) and provider (lubimyczytac.provider.test.ts)pnpm run verify:fastpasses (lint, typecheck)Anything non-obvious in the diff?
The scraper targets public lubimyczytac.pl pages. Between-request delay set to 600ms (
LUBIMYCZYTAC_BETWEEN_REQUESTS) to be polite to the site.Checklist
.env, personal configs)Summary by CodeRabbit
New Features
Tests
Chores