Skip to content

fix(importer): reject a download whose file is in a language the profile does not allow (#2998) - #3000

Merged
vavallee merged 2 commits into
mainfrom
fix/2998-reject-disallowed-language
Oct 4, 2026
Merged

vavallee merged 2 commits into
mainfrom
fix/2998-reject-disallowed-language

Conversation

@vavallee

@vavallee vavallee commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

A release whose name does not say its language passes the grab time language filter. At import, #1933's applyEmbeddedLanguage then read the EPUB's dc:language and relabelled the book to match, so an English only library filled with Swedish and Dutch editions that showed as imported and were never searched again (65 of 4,500 books in the report). Now, when the book's allowed languages are an explicit list and the downloaded EPUB declares a specific language outside it, the release is rejected the same way a disallowed format is: blocklisted, download blocked with a message naming both languages, book left Wanted.

Closes #2998.

Implementation notes

Design decision: on by default for explicit lists, no new setting

The issue suggested a setting so people who like today's relabelling keep it. Those people are the ones whose profile allows any language, and for them nothing changes: an empty or any profile resolves to "no restriction" and #1933's relabelling runs exactly as before. Enforcement fires only where the user has already written down a list of allowed languages, and that same list already hard filters release names at grab time. The import check is that filter applied to better evidence (the file instead of its name), so a file is never held to a stricter rule than its name would have been. A toggle would only let someone keep a restriction at search time while waiving it at import, which is the bug. No setting, no migration, no web change.

Where the allowed list comes from

importAllowedLanguages mirrors Scheduler.searchAndGrabFormat and IndexerHandler.SearchBook exactly:

Situation Enforced list
Author's metadata profile has allowed_languages (default "Standard" profile is eng) that list, via ParseAllowedLanguages
Profile is any or empty, search.preferredLanguage = en eng (the only value indexer.FilterByLanguage acts on)
Profile is any or empty, preferred language unset or anything else none, relabelling as today
Profile cannot be loaded falls through like the scheduler does; never rejects on a load error

What counts as a disallowed file

  • Every dc:language of every EPUB the import loop would place counts (EpubMetadata.Languages; Language stays the first for existing callers). An EPUB is allowed when any declared language is allowed, disallowed when it declares a specific language and none is allowed, unknown otherwise. The release is rejected only when some EPUB is disallowed and none is allowed, the shape of the format gate. A release mixing allowed and disallowed EPUBs imports the allowed ones; the ebook loop skips the others (languageCheck.skip), exactly where it skips a disallowed format. Under a restricted profile, relabelling picks the first allowed declared language.
  • Compared with models.IsLanguageAllowed, which normalises both sides, so en, en-US, en_GB, EN, eng all match eng.
  • Only a definite language can be rejected: und, mul, zxx, mis, qaa..qtz, missing, and anything that is not a 3 letter code after normalisation (unknown, x-default) import as today.
  • iso639TwoLetterToB now covers all 183 ISO 639-1 codes (it held 25, so uk/he/sk passed as "no language"), languageNames has an English name for every code produced, and cmn/yue fold onto chi.
  • nb/nob/nn/nno now fold onto nor in NormalizeLanguageCode via iso639NorwegianToB, matching francisrath's feat(metadata): Nasjonalbiblioteket as an opt-in primary provider (#2979) #2995 hunk line for line (credited as co author). The editor offers only nor, so a Norwegian profile used to reject Bokmål tagged books in the catalogue filter too; folding there fixes every caller at once. LanguageCodeVariants gains the individual codes for provider side filtering.
  • Language lock: if the user locked the book's language, that language is added to the allowed set for this book only. The user said "this book is in X", which beats the author wide profile; it does not widen to other languages (a German file for a book locked to Swedish under an eng profile is still rejected).

How rejection reuses the format enforcement path (#1782)

Wired through WithLanguageEnforcement(metadataProfileRepo, blocklistRepo) next to WithFormatEnforcement in cmd/bindery/main.go; unwired scanners (every existing test, any other caller) behave as before. The gate sits right after the format and multi book guards, before anything is placed, and calls the same helpers in the same order: recordUnmatchedImportPath (so Match to book can force it in), blocklistRejectedRelease, failImport(StateImportBlocked, reason), which writes the importFailed history row, sets the download status and sends the downloadFailed notification. Message example: file declares Swedish (swe), but the metadata profile "Standard" allows only English (eng). Not imported, and the release was blocklisted so the next search picks another. To keep this file anyway, use Match to book in the Queue or manual import. models.LanguageName supplies the names, with a test that every code the alias tables produce has one.

Manual import, scans, existing books

ImportFromPath and ImportFilesFromPath (manual import, Reassign, Match to book, batch import; every caller is a user action in internal/api) mark the context as manual. A manual import with a disallowed language logs a warning and imports, and relabelling records the real language as before. The format check treats formatHint as its override instead; I did not change that here. Library scans and adoption never run tryImportInternal, and nothing touches already imported books. External import mode returns before files are inspected, so it is unchanged (documented).

Checklist

  • Tests added or updated
  • Doc-update gate cleared: docs/User-Guide-Wiki.md (Import section), docs/Troubleshooting-Wiki.md (A grabbed book never imports), godoc on new symbols
  • Wiki pages updated if user-facing behaviour changed (the in repo wiki docs are updated; the GitHub wiki mirror, if any, is not)

Test plan

Fail before: the new importer tests run against origin/main (with a compile shim for WithLanguageEnforcement) fail RejectsDisallowedLanguage, PreferredLanguageFallback and LockedLanguageDoesNotWidenToOthers with download status = "imported", want "importBlocked"; the new nb/nob/nn/nno cases in TestNormalizeLanguageCode fail on main ("nob" = "nob", want "nor"). All pass on this branch.

  • go test ./internal/importer/ -run TestLanguageEnforcement: English only profile + Swedish EPUB is blocked, blocklisted, book stays Wanted and eng, no file placed, no correction event, one importFailed row, downloadFailed sent; allowed languages in 12 spellings import (incl. en_GB, de-AT vs ger, nob vs nor); undeclared/und/mul/zxx/unknown import; any and empty profiles keep relabelling; preferred language fallback; manual single and multi file import not rejected; locked language allowed and not widened; unwired scanner unchanged
  • Review reproductions, failing before the second commit: fr,en EPUB blocked (importBlocked ... file declares French), und,en-GB relabelled the book und, A sv + B en rejected, uk/he/sk/fa-IR/is all imported under eng. All pass now, plus all-disallowed multi EPUB rejection naming every language
  • go test ./internal/models/ (normalisation, LanguageName, name coverage, all 183 ISO 639-1 codes)
  • go test ./cmd/... ./internal/... (full suite)
  • gofmt -l, go vet ./cmd/... ./internal/..., golangci-lint run v2.11.4: 0 issues
  • make smoke (not run)

Follow-ups (not in this PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9

…ile does not allow (#2998)

The language filter at grab time reads only the release name, so a
release that does not say its language passes it. At import #1933 then
read the EPUB's dc:language and relabelled the book to match, so an
English only library quietly filled with Swedish and Dutch editions that
showed as imported and were never searched again.

When the author's metadata profile lists its allowed languages (or, with
an any language profile, search.preferredLanguage is "en", mirroring the
grab time filter) and the downloaded EPUB declares a specific language
outside that list, the release is now handled like a disallowed format:
import path recorded for Match to book, release blocklisted, download
blocked with a message naming both languages, book left Wanted, nothing
placed or relabelled. A user locked book language counts as allowed for
that book.

Unchanged: undeclared, und, mul and unmapped codes; any language
profiles (relabelling stays); manual import and Match to book (warn
only); library scans and adoption; books already imported.

Also folds nb/nob/nn/nno onto nor in NormalizeLanguageCode so Bokmal
and Nynorsk files pass a Norwegian profile, and adds LanguageName for
readable messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.40741% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/importer/language_enforcement.go 84.61% 7 Missing and 7 partials ⚠️
internal/models/language.go 83.33% 1 Missing and 1 partial ⚠️
internal/importer/scanner.go 96.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

…#2998)

Review of #3000 found two holes in the import language check.

Only the first dc:language of the first EPUB counted, so an EPUB tagged
fr then en was blocked under an English profile while the same file
tagged en then fr imported, and a release with A.epub (sv) and B.epub
(en) was rejected while the reversed names imported. EpubMetadata now
also carries Languages (every dc:language, normalised, deduped; Language
is unchanged for existing callers). The gate classifies each candidate
EPUB as allowed when any declared language is allowed, and rejects the
release only when some EPUB is disallowed and none is allowed, the
shape of the format gate. A mixed release imports the allowed EPUBs and
skips the rest in the ebook loop, as the format check skips a format.
Relabelling under a restricted profile picks the first allowed declared
language, so an "und, en-GB" EPUB no longer relabels the book "und".

The two letter table held 25 codes, so uk, he, sk, fa, is and the rest
normalised to nothing known and passed as "no language". It now covers
all 183 ISO 639-1 codes, with English names for every code it yields,
and cmn/yue fold onto chi.

The Norwegian fold is renamed iso639NorwegianToB and matches
francisrath's #2995 hunk line for line so his rebase drops it cleanly.

Docs note that External import mode is not checked.

Co-authored-by: francisrath <fr@ncis.no>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
@vavallee
vavallee marked this pull request as ready for review October 4, 2026 23:17

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix. The enforcement logic is correctly scoped (EPUB-only, opt-in via WithLanguageEnforcement, falls through on load errors), the relabelling behaviour is preserved for unrestricted profiles, and the test coverage is thorough — the 12-spelling roundtrip cases, mixed-release partial-import, locked-language widening check, and the reproduction list all read as genuine regression guards.

Two items worth a look before merge:

1. Shared blocklist field between the two enforcement methods — WithFormatEnforcement and WithLanguageEnforcement both write to s.blocklist (scanner.go). In main.go both calls receive the same blocklistRepo instance so there is no bug today, but the coupling is invisible at the call site. If a future caller constructs a scanner with only one of the two wired (which is a normal pattern — tests do exactly this), the second With… call would silently overwrite the first method's blocklist. Because blocklistRejectedRelease guards on s.blocklist == nil, an accidentally-nil write from the language path would also disable blocklisting for format-rejected releases with no error. Consider either promoting blocklist to a separate WithBlocklist setter that both enforcement methods share, or documenting the coupling prominently on the field.

2. Double-normalize in languageList — language_enforcement.go:460 calls NormalizeLanguageCode(code) and then passes the result n to LanguageName(n), which calls NormalizeLanguageCode again internally. Both inputs (lc.declared and profileAllowed) are already normalised three-letter codes, so all three calls are idempotent and the output is correct. But the pattern muddies the input contract of languageList. languageList(codes []string) could just call models.LanguageName(code) for the name part and models.NormalizeLanguageCode(code) for the display code, with a godoc note that codes are normalised before rendering.

Neither is a blocking defect and the smoke run being skipped is noted in the checklist. The rest of the diff — the ISO 639-1 completion, the Norwegian fold, the EpubMetadata.Languages slice, the definiteLanguage private-use range guard — all look correct.

— 🤖 Bindery triage bot (automated). Reply to correct me; a human will see it.

@vavallee
vavallee merged commit 5f2ea06 into main Oct 4, 2026
43 of 44 checks passed
@vavallee
vavallee deleted the fix/2998-reject-disallowed-language branch October 4, 2026 23:23
francisrath added a commit to francisrath/bindery that referenced this pull request Oct 5, 2026
…now ships

The nb/nob/nn/nno fold landed on main with vavallee#3000, which carries its own
changelog entry for it, so the release notes would list it twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>
vavallee added a commit that referenced this pull request Oct 6, 2026
…ne (#2998)

Review of the first commit: counting an undeclared (none, und, mul) EPUB
as acceptable let a release of a Swedish edition plus the same edition
with its metadata stripped import under an English only profile, be
marked done and never searched again, the #2998 symptom.

An undeclared EPUB now imports on its own or beside an EPUB in an allowed
language, as on main, and no longer rescues a release whose only
declared language is disallowed. The allowed plus disallowed skip path
from #3000 is unchanged. The iw/in/ji normalisation stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
vavallee added a commit that referenced this pull request Oct 6, 2026
* fix(importer): import an untagged epub beside a disallowed-language one (#2998)

The language gate counted an EPUB declaring no specific language (none,
und, mul) as neither allowed nor disallowed, so a release holding one
plus a Swedish EPUB under an English only profile was rejected whole,
although the untagged file alone imports. It now counts as acceptable:
it is imported and the Swedish one skipped, the shape of the format gate.

NormalizeLanguageCode also folds the withdrawn ISO 639-1 codes iw, in
and ji onto he, id and yi. They used to pass through raw, read as no
language, and relabel the book to "iw".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

* fix(importer): let adoption take over a file left by a deleted book (#2937)

PathOwnedByOtherBook counts a book_files row whose book no longer exists
as owned. That is right for the delete guards, but adoption's pre-check
and the existing file bind on add author used it too, so adoption gave a
409 naming no book and add author searched for a file it already had,
while the write behind both (Track) would take the orphan row over.

Add PathOwnedByLiveOtherBook, which joins books and ignores an orphan
row, and use it only in AdoptionHandler.checkOwnership and
existingFileOwnedByOtherBook. The delete guards (path_safety.go),
reassign cleanup (manual_import.go) and the reorganize collision check
keep the orphan counts as owned behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

* fix(importer): recognise punctuated placeholder and publisher author credits (#2942)

isPlaceholderAuthor only trimmed outer brackets and folded case, so
"Various Artists.", "Various Authors;", "Unknown Author(s)", "Full-Cast"
and "Anon." read as authors, and publisher credits such as "Brilliance
Audio", "Recorded Books", "Tantor Audio" or "HarperAudio" were not on the
list at all. Each raised an author conflict with the folder and an Add
author for nobody.

Fold the tag before comparing (brackets, dots, slashes and apostrophes
dropped, other punctuation read as a space) and extend the single table
with "Author Unknown" and the common audiobook publishers and imprints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

* fix(importer): keep blocking an undeclared epub beside a disallowed one (#2998)

Review of the first commit: counting an undeclared (none, und, mul) EPUB
as acceptable let a release of a Swedish edition plus the same edition
with its metadata stripped import under an English only profile, be
marked done and never searched again, the #2998 symptom.

An undeclared EPUB now imports on its own or beside an EPUB in an allowed
language, as on main, and no longer rescues a release whose only
declared language is disallowed. The allowed plus disallowed skip path
from #3000 is unchanged. The iw/in/ji normalisation stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

---------

Signed-off-by: vavallee <vavallee@protonmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vavallee pushed a commit that referenced this pull request Oct 7, 2026
) (#2995)

* feat(metadata): add Nasjonalbiblioteket provider

Read-only client for the National Library of Norway's catalogue search
API (api.nb.no/catalog/v1/items). NB holds Norwegian publications by
legal deposit under their original titles, where OpenLibrary often
catalogues them under the English translation.

NB records are editions, not works, so author catalogues are folded into
works by title, with translations joined to the original through the
original or uniform title the cataloguer recorded. Authors are keyed by
their Norwegian authority file ID; NB search cannot look an author up by
that ID, so the name is resolved through authority.bibsys.no.

No cover or series-catalogue capability: NB's image service refuses
in-copyright items, and series positions are only in the per-record MODS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* feat(metadata): wire Nasjonalbiblioteket as an opt-in primary provider

"nb" is accepted for metadata.primary_provider and nb: foreign IDs route
to it. Unlike the other providers it is not registered as an enricher
when another provider is primary, so installs that did not choose it send
the National Library no traffic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* feat(web): offer Nasjonalbiblioteket in the primary metadata selector

Adds the provider to the Metadata Profiles selector with help text, names
nb: records in the provider labels, and links NB book records to their
nb.no page. Docs and changelog fragment updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): return the whole work from the NB book lookups

The aggregator resolves an ISBN to the primary's work and refreshes it
through GetBook, which returned only the one edition record, so a book
added by ISBN lost its other editions (an audiobook's ISBN among them).
GetBook now finds the record's siblings by authority-file author and
title, falling back to the record alone if that search fails.

Book search also accepts the "isbn:<n>" form the canonical lookup sends,
and includes audiobooks like the author catalogue does, so both paths
return the same editions for a work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* feat(metadata): series and series numbers from Nasjonalbiblioteket

NB's search records name a series but not the number; the number is only
in each record's MODS. Works whose records name a series get one MODS
request (bounded, at most three editions per work, four in flight).

Only the author's own series is taken: the MODS series entry linked to
their authority record. Publisher imprint series are recorded as series
too but never carry that link. An entry recorded without the link is
accepted only when the same series is linked elsewhere in the catalogue.
Series are filled in the author catalogue and in GetBook, because a
rebind replaces a book's series with what GetBook returns.

Language qualifiers on catalogue titles are now matched against a list of
language names instead of a suffix pattern, which also matched names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* docs(changelog): key the Nasjonalbiblioteket fragment to #2979

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): read NB volume titles and numbers from the part fields

NB catalogues some volumes under their series' name: the title proper is
the series, the volume's own title is the part name, and the number is
the part number, sometimes only on the uniform title. Taking the title
proper turned each such volume into a book named after its series, with
no number, since its search record names no series to look up.

The part name is now the record's title, and the part number is a series
candidate, taken on the same condition as an unlinked series entry: the
author's catalogue links that series elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): recover NB series volumes missing from the name index

NB leaves some records out of every name index although their author
credit is correct, so no name search returns them and a series came back
with volumes missing. A search on each series the author's catalogue
links them to finds them; only records crediting the author's authority
ID are kept. One extra request per series, and MODS lookups are cached
for the call so regrouping fetches nothing twice. A failed recall search
marks the catalogue partial, so reconciliation never reads a volume
found this way as removed upstream.

The single-book sibling search drops the name-index filter for the same
reason; it already keeps only the author's records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): fold NB records whose title a cataloguing slip split

A record whose title a cataloguing slip had split with a space came back
as a second book beside the same work. Work keys now ignore spaces
(within one author, so two of their titles differing only in spacing are
taken as one work), and a work takes the title most of its editions
carry, so one record's slip cannot name the book.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* docs(changelog): match the fragment style for #2979

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): mark NB rate limits and outages as provider errors

HTTP 429 now wraps providererr.ErrRateLimited and 5xx wraps
providererr.ErrUnavailable, so scheduled discovery backs off instead of
walking on through its queue. NB publishes no limits, so a refusal is
taken at its word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(api): use NB works' own editions in catalogue reconciliation

Reconciliation looked up each work's editions with a per-work call, which
for NB is a full GetBook plus a sibling search. NB works come from the
author catalogue with every edition already attached, so those are the
evidence now and the call is skipped, as it already is for DNB. Unlike
DNB, the works are still judged: a work with no editions attached stays
indeterminate rather than counting as having no ISBN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): report an empty NB catalogue for a known author as partial

When the authority record exists but the name search returns nothing, the
author exists and NB's name index simply misses their records, so the
catalogue is reported incomplete instead of complete and empty, which
reconciliation could otherwise read as every book removed upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* docs: record Nasjonalbiblioteket's data terms and what leaving it as primary means

Adds a Nasjonalbiblioteket section to third-party-data.md: CC0 records via
NB's metadata delivery, publisher-written summaries, no covers, and the
Sikt authority file under NLOD 2.0 with its attribution line. The user
guide and architecture notes now say that switching the primary away from
NB leaves NB-linked authors unsynced until they are relinked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): report a missing NB authority record as a partial catalogue

A 404 or deleted authority record returned an empty catalogue marked
complete. Sikt merges duplicate authority records now and then, and
reconciliation would then offer to remove every wanted book of that
author. It is now reported as partial, like an empty name search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(api): judge NB works on their own editions only on a complete catalogue

On a partial catalogue (over the page cap, or a failed series recall) a
work can be missing editions, so a lone short edition made it a page
count candidate. Its own editions are now the evidence only when the
snapshot is complete; otherwise the work stays indeterminate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* docs: cite the CC0 source for NB records and fix the GetEditions comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* docs(changelog): drop the Norwegian language entry that #2998 now ships

The nb/nob/nn/nno fold landed on main with #3000, which carries its own
changelog entry for it, so the release notes would list it twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* feat(metadata): narrator, running time, genres and name variants from NB

All of it comes from requests NB already serves: the catalogue records a
work is built from, and the authority record GetAuthor reads.

- Narrator: the narrator credits on the work's audiobook editions.
- Running time: parsed from the audiobook record's extent ("1 lydfil
  (11 t, 16 min)", "21:34:00", a CD set's "(3 t, 7 min)"), on the
  audiobook edition and the book. Records that state none get none.
- Genres: the cataloguer's subject genres, without format terms and
  without the Nynorsk twin of a Bokmål term NB lists beside it.
- Name variants: the authority record's other name forms become the
  author's AlternateNames, which the add-author flow keeps as aliases
  when they are another spelling of the same name (LatinAliasBinds), so
  a release name written without Norwegian letters still matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* test(metadata): cover NB edge cases and failure paths

Not-found lookups answer nothing rather than an error, unusable input
makes no request, a transport failure is an error and never an empty
catalogue, and malformed authority links, single names, unusable years
and sparse records are handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): fetch NB works from the creator index and count creator credits

NB leaves many records that credit an author out of its nameauthor
index, and catalogues authorship as the generic "cre" about as often as
"aut". For some authors the provider saw a small fraction of their
catalogue. Fetch from namecreators, accept "cre" and "creator" as author
credits, and raise the page cap to 20, since the creator index counts
every translation. The authority-ID check still drops translator and
narrator credits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* fix(metadata): merge NB series named two ways and keep translation names out

NB catalogues one series under more than one name ("<A> og <B>" and
"<full name A> og <full name B>"), linking both to the author, so a
series came out split in two. Names that one work's records link with
the same number are now one series, under the name most books carry.

Since works include their translations, the series probe could land on
a translation's record and name the series in that language. Series now
come from the book's Norwegian records first, then Danish and Swedish
ones, which name series alike. A record that numbers the volume is
preferred over one that does not.

Some series are linked on a translation's record alone, with no
Norwegian record naming them. Those are used as a last resort, after
every Norwegian source. This sometimes has strange effects: a Norwegian
book can end up in a series with a Finnish, English or Dutch name, and
one series can appear under two names when neither record numbers the
volume. The books are still grouped and mostly numbered correctly, which
is better than no series at all. On 28 Norwegian authors, the fallback
recovered 17 books into a correctly named series and gave 9 a series
with a foreign name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

* docs(metadata): say NB's search API states no licence; CC0 is the delivery's

The CC0 statement we cited covers NB's metadata delivery over OAI-PMH
and SRU, which Bindery does not use. The catalogue search API it reads
publishes no licence of its own. Say so in the third-party data page and
the package comment, and note that the NLOD attribution for the
authority file applies when the data is redistributed, not to a
self-hosted install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Francis Rath <fr@ncis.no>

---------

Signed-off-by: Francis Rath <fr@ncis.no>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reject an import whose file declares a language the profile doesn't allow (instead of relabelling the book)

1 participant