Librarry: How a Go Backend Manages Ebooks with Resilient Metadata
Librarry is a self-hosted book manager for ebooks and audiobooks. It is still early alpha, but the backend is already interesting because it treats metadata as the hard part of book automation rather than as a thin wrapper around one upstream API.
The project is aiming at Readarr-style workflows: search for books, monitor authors, mark titles as wanted, evaluate releases, hand downloads to existing clients, import completed files, and preserve manual corrections when provider data is incomplete or wrong. That gives us a useful Go codebase to study, because it has real integration pressure without trying to hide everything behind one giant package.
The backend shape
The Go application lives under backend/, with backend/cmd/librarry/main.go as the executable entrypoint. The entrypoint loads environment configuration, opens Postgres when LIBRARRY_DATABASE_URL is set, applies SQL migrations, creates services, and wires everything into the HTTP router.
Most of the application code sits under backend/internal/. That matters. Go gives directories named internal special import rules, so packages outside the parent tree cannot import them. The official Go module layout documentation recommends this for code that supports your application but is not meant to be a public API.
Librarry uses that boundary for the main backend packages:
backend/internal/metadata/handles provider search, matching, merging, diagnostics, and metadata types.backend/internal/wanted/owns wanted items, author monitoring, release evaluation, failed-download recovery, upgrades, and history.backend/internal/acquisition/talks to Prowlarr, qBittorrent, Transmission, and SABnzbd.backend/internal/library/scans roots, extracts local metadata, tracks files, imports completed downloads, and handles rename flows.backend/internal/api/defines native and Readarr-compatible HTTP routes.backend/internal/compat/stores Readarr-style resources such as root folders and config records.backend/internal/calibre/handles Calibre Content Server integration.backend/internal/config/andbackend/internal/database/keep environment parsing, Postgres setup, and migrations away from business logic.
That package list is not fancy, but it is practical. Each package owns a clear boundary, and the service wiring in main.go makes those boundaries visible.
Metadata starts with providers
Book metadata gets messy quickly. A title might have multiple editions, several ISBNs, inconsistent author names, a missing cover, or audiobook-specific details that do not line up with ebook data. Librarry’s metadata package starts by accepting that reality.
backend/internal/metadata/providers.go defines the provider setup. The default provider list includes Hardcover, Open Library, Google Books, and a local OPF provider. Hardcover and Google Books can use credentials from environment variables, Open Library is available without a project-specific key, and local metadata gives the app a fallback when remote records are thin.
The orchestration lives in backend/internal/metadata/service.go. SearchDetailed trims and normalizes the request, calls each provider, keeps provider errors separate from successful results, merges equivalent records, filters by preferred language, sorts by score, and applies the requested limit.
That is a better shape than failing the whole search because one provider is unavailable. In a media manager, partial data is still useful. The UI can show the best available candidates while also reporting that one source failed.
Matching and merging are explicit
The matching code in backend/internal/metadata/match.go is deliberately straightforward. It normalizes strings, recognizes ISBN queries, scores exact and partial title matches, adds author evidence, and classifies confidence as high, medium, or review.
The merge code in backend/internal/metadata/merge.go then clusters compatible results. Shared ISBNs can merge directly. Otherwise, the code compares normalized title and author values, avoids merging incompatible formats, and keeps publication years within a small tolerance when both sides provide them.
When multiple providers agree, the merged result gets extra evidence via provider corroboration. The richer candidate becomes the base, then missing fields from the other candidates can fill gaps. This is the kind of boring code that makes a product feel reliable: no magic AI matching step, just explicit rules that can be tested and debugged.
Acquisition is scoped, not overbuilt
Librarry is not trying to replace qBittorrent, Transmission, or SABnzbd. Its README is clear about that boundary: those tools remain the download clients, while Librarry owns the book workflow around them.
That boundary shows up in backend/internal/acquisition/. interfaces.go defines request and status types for release search, download grabs, queue state, details, files, trackers, peers, categories, tags, and preferences. prowlarr.go searches indexers. qbittorrent.go, transmission.go, and sabnzbd.go map the client-specific APIs into Librarry’s book-acquisition model.
The service in backend/internal/acquisition/service.go chooses the right client, handles categories for ebook and audiobook downloads, resolves Prowlarr release payloads, and rejects requests that do not fit the selected client. For example, torrent uploads require a torrent-capable client rather than being forced through SABnzbd.
This is a good Go pattern: keep the external-client weirdness close to the integration files, and let the rest of the app work with application-level concepts like releases, downloads, categories, and import status.
Wanted items connect metadata to downloads
The original draft of this post barely mentioned backend/internal/wanted/, but that package is central to the app.
Wanted items are the bridge between “this book exists” and “go find a release for it.” The wanted service can create and update wanted records, list quality profiles, subscribe to authors, run author monitoring, evaluate releases, search upgrades, recover failed downloads, and keep history.
It also owns manual metadata correction. Provider data can be wrong, and Librarry stores protected overrides so later provider refreshes do not casually overwrite a human fix. That is important for books, where title variants, series positions, languages, publishers, and ISBNs often disagree across sources.
Library import uses local evidence too
Remote providers are not the only source of truth. backend/internal/library/embedded_metadata.go reads metadata from local files and sidecars during scan/import.
For ebooks, it can parse OPF files directly, find sidecar OPF files, and open EPUB archives to locate the package document referenced by META-INF/container.xml. For audiobooks and audio files, it handles common extensions such as .mp3, .m4a, .m4b, and .aac, reading ID3v2 or MP4-style tags where present.
That local evidence feeds the import workflow. It can help match an unlinked completed download to a wanted item, avoid guessing from a filename alone, and make manual review less painful when there is not enough confidence to import automatically.
Readarr compatibility lives at the edge
The API package exposes both native routes and a large set of Readarr-compatible /api/v1 routes. backend/internal/api/router.go wires health checks, metadata search, wanted items, downloads, imports, settings, notifications, release search, root folders, history, queue, commands, tags, quality profiles, and config resources.
The compatibility code is kept near the API and compat store instead of leaking through the whole backend. That is the right place for it. Readarr compatibility is a product requirement, but it should not force every internal type to look like an Arr API response.
Calibre follows a similar boundary. The backend accepts Calibre-related root-folder settings and can hand imported files to a Calibre Content Server, but that integration lives behind backend/internal/calibre/ and the library service rather than being mixed into generic metadata search.
What Go developers can steal
A few patterns are worth taking from Librarry even if you are not building a media app.
Use internal/ for application code you do not want to support as an importable API. The Go toolchain enforces that boundary, which gives you more freedom to refactor.
Keep provider failures separate from the overall result. Librarry can return useful metadata even when one upstream source fails, and it can still tell the caller which provider had a problem.
Make matching rules visible. ISBN normalization, title scoring, author evidence, language filtering, and provider corroboration are easier to reason about when they are regular Go functions with tests.
Put integration code behind application concepts. The acquisition package does not make the whole backend speak qBittorrent, Transmission, or SABnzbd. It translates those APIs into releases, downloads, categories, files, trackers, and actions.
Treat manual correction as durable data. If users fix metadata, later provider syncs should preserve that decision unless the user explicitly resets it.
Librarry is young, but its backend is already shaped around the problems that make book automation difficult: unreliable metadata, multiple formats, external download clients, imports that need evidence, and compatibility with an existing ecosystem. That makes it a useful Go repository to read before designing your own integration-heavy backend.