Repository navigation
fix(importer): reject a download whose file is in a language the profile does not allow (#2998) - #3000
Conversation
…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 Report❌ Patch coverage is 📢 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>
There was a problem hiding this comment.
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.
…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>
…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>
* 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>
) (#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>
Summary
A release whose name does not say its language passes the grab time language filter. At import, #1933's
applyEmbeddedLanguagethen read the EPUB'sdc:languageand 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
anyprofile 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
importAllowedLanguagesmirrorsScheduler.searchAndGrabFormatandIndexerHandler.SearchBookexactly:allowed_languages(default "Standard" profile iseng)ParseAllowedLanguagesanyor empty,search.preferredLanguage=eneng(the only valueindexer.FilterByLanguageacts on)anyor empty, preferred language unset or anything elseWhat counts as a disallowed file
dc:languageof every EPUB the import loop would place counts (EpubMetadata.Languages;Languagestays 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.models.IsLanguageAllowed, which normalises both sides, soen,en-US,en_GB,EN,engall matcheng.und,mul,zxx,mis,qaa..qtz, missing, and anything that is not a 3 letter code after normalisation (unknown,x-default) import as today.iso639TwoLetterToBnow covers all 183 ISO 639-1 codes (it held 25, souk/he/skpassed as "no language"),languageNameshas an English name for every code produced, andcmn/yuefold ontochi.nb/nob/nn/nnonow fold ontonorinNormalizeLanguageCodeviaiso639NorwegianToB, 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 onlynor, so a Norwegian profile used to reject Bokmål tagged books in the catalogue filter too; folding there fixes every caller at once.LanguageCodeVariantsgains the individual codes for provider side filtering.engprofile is still rejected).How rejection reuses the format enforcement path (#1782)
Wired through
WithLanguageEnforcement(metadataProfileRepo, blocklistRepo)next toWithFormatEnforcementincmd/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 theimportFailedhistory row, sets the download status and sends thedownloadFailednotification. 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.LanguageNamesupplies the names, with a test that every code the alias tables produce has one.Manual import, scans, existing books
ImportFromPathandImportFilesFromPath(manual import, Reassign, Match to book, batch import; every caller is a user action ininternal/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 treatsformatHintas its override instead; I did not change that here. Library scans and adoption never runtryImportInternal, and nothing touches already imported books. External import mode returns before files are inspected, so it is unchanged (documented).Checklist
docs/User-Guide-Wiki.md(Import section),docs/Troubleshooting-Wiki.md(A grabbed book never imports), godoc on new symbolsTest plan
Fail before: the new importer tests run against
origin/main(with a compile shim forWithLanguageEnforcement) failRejectsDisallowedLanguage,PreferredLanguageFallbackandLockedLanguageDoesNotWidenToOtherswithdownload status = "imported", want "importBlocked"; the newnb/nob/nn/nnocases inTestNormalizeLanguageCodefail 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 andeng, no file placed, no correction event, oneimportFailedrow,downloadFailedsent; allowed languages in 12 spellings import (incl.en_GB,de-ATvsger,nobvsnor); undeclared/und/mul/zxx/unknownimport;anyand empty profiles keep relabelling; preferred language fallback; manual single and multi file import not rejected; locked language allowed and not widened; unwired scanner unchangedfr,enEPUB blocked (importBlocked ... file declares French),und,en-GBrelabelled the bookund,A sv + B enrejected,uk/he/sk/fa-IR/isallimportedundereng. All pass now, plus all-disallowed multi EPUB rejection naming every languagego 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 runv2.11.4: 0 issuesmake smoke(not run)Follow-ups (not in this PR)
TLAN/ MP4 language atom is not checked yet.applyEmbeddedLanguagestill relabels a book toundormulwhen the EPUB says so; it should skip non specific codes too.formatHint, so a format rejected (Quality profile format enforcement exists only at grab time — the importer never checks the actual downloaded file against the allowed-formats list #1782) release cannot be forced in through it; the manual marker added here could be its override as well.🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9