A map of the codebase: layout, layering, conventions, contradictions, and open challenges. Static analysis only — no runtime/operational notes. File-and-line citations are accurate against the tree at the time of writing.
Snapshot: describes Librarr at the
1.1.0-betarelease (main, 2026-07-30). Version-, count- and pin-bearing lines were re-verified against the tree on 2026-07-30; treat anything older as suspect. The shipping-version references below were bumped for the1.2.6-betatag on 2026-10-12, but nothing else in this file was re-verified then — the counts and citations still date from 2026-07-30. Inherits its codebase from upstreamReadarr/ReadarratdevelopHEAD0b79d300("Retirement announcement", 2025-06-27); last upstream tagged release wasv0.4.18.2805(commit7cc02f95, 2025-06-10). The upstream repository is archived on GitHub.
Status of the underlying Readarr code: archived upstream. This document still describes that inherited code map. For a focused inventory of what Librarr added on top, jump to § Librarr fork additions immediately below.
Code paths added or materially changed in the Librarr fork on top of the
inherited Readarr tree. Everything here is new since upstream
0b79d300; the sections that follow this one describe the inherited
codebase. See CHANGELOG.md for the per-cycle history
of how this material landed.
src/NzbDrone.Core/MetadataSource/OpenLibrary/. Native HTTP client +
search services + response→domain mappers — no rreading-glasses
shim and no bookinfo.club dependency.
OpenLibraryProxy.cs— HTTP proxy,AllowAutoRedirect = truerequest shape (the explicit-opt-in pattern that Cycle 7a later ported to the NZB grab path).OpenLibraryAuthorSearchService.cs,OpenLibraryBookSearchService.cs— search by name / title / identifier shape.OpenLibraryIdHelper.cs—IsAuthorId/IsWorkIdshape checks promoted out ofMetadataSourceFactoryso other callers can share the canonical OL ID predicates.Mappers/AuthorMapper.cs,BookMapper.cs,EditionMapper.cs— manual mapping from OL response shapes toAuthor/Book/Editiondomain entities (no AutoMapper, matching the rest of the codebase).
Migration src/NzbDrone.Core/Datastore/Migration/041_book_id_mapping.cs.
Confidence-scored GoodReads-ID → OpenLibrary work/edition mappings,
registered in TableMapping.cs (Cycle 4) and consumed by
MetadataSourceFactory.cs on every refresh (Cycle 5). The reverse
lookup (GoodreadsId → OL IDs) dominates the access pattern during
legacy data migration. Backed by BookIdMappingRepository.cs.
src/NzbDrone.Core/Books/Services/ReidentifyService.cs— walks every book, matches against OpenLibrary using ISBN / ASIN / title-author confidence, persists results intoBookIdMapping.src/NzbDrone.Core/Books/Commands/ReidentifyLibraryCommand.cs— the command that triggers it. Manually runnable, or auto-enqueued by the first-boot migration below.
src/NzbDrone.Core/Books/Services/LegacyMigrationService.cs— handlesApplicationStartedEvent+CommandExecutedEvent. On first boot of a legacy DB, it: (1) detects GoodReads-shaped IDs, (2) flipsMonitorNewItemstoNoneper-author (so the OL refresh path doesn't grab unwanted editions mid-migration), (3) enqueuesReidentifyLibraryCommandat high priority and stashes the command ID, (4) on the matchingCommandExecutedEventCompleted status, persists theLegacyMigrationCompletedmarker.- Config surface:
LegacyMigrationCompleted+LegacyMigrationVersioninConfigService.cs.LegacyMigrationService.CurrentVersiondefines the schema version. src/NzbDrone.Core/HealthCheck/Checks/LegacyMigrationCheck.cs— state machine: marker set → Ok; no legacy authors → Ok; command queued/running → Notice; legacy authors + no command → Warning.
frontend/src/App/LegacyMigrationBanner.js+.css+Connector.js— pollsstate.system.health.itemsfiltered tosource === 'LegacyMigrationCheck'andstate.commands.itemsfor activeReidentifyLibrarycommands every 15 s. RendersAlert kind=INFOwhile running,WARNINGwhile pending, auto-hides on Ok.- Wired into
frontend/src/Components/Page/Page.js:94directly below<PageHeader>.
- Migrations
042_edition_narrators.cs,043_normalized_narrators.cs,044_drop_editions_narrators.cs— additive normalized narrator schema, then drop the legacy CSV column once the join is live. NarratorService.cs(underBooks/Services/), wired intoRefreshEditionService.cs.- REST surface:
Readarr.Api.V1.Narrators(NarratorController.cs+NarratorResource.cs), with a per-narrator detail page on the frontend.
- Migration
045_book_preferred_cover_url.cs—Book.PreferredCoverUrlcolumn. - Frontend modal lets the user pick from candidate OL covers; the canonical OL cover is the default.
scripts/bench_le_guin.py— offline harness for evaluating cover-pick quality against a held-out fixture set.
src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs:24—.azw(Kindle KF7) mapped toQuality.MOBI. Same Mobipocket container as.mobi; extraction path is identical. (Cycle 7c.)
src/NzbDrone.Core/Download/UsenetClientBase.cs — sets
request.AllowAutoRedirect = true on the NZB grab. Required because
locally-built docker images don't run as Azure officialBuild, so
the dev-mode redirect-rejection guardrail in HttpClient.cs:101 was
rejecting every NZBgeek / NzbPlanet CDN-redirect grab. (Cycle 7a.)
src/NzbDrone.Core/MediaFiles/BookImport/ImportApprovedBooks.cs's
Import() method — for every rejected ImportDecision, materializes
an ImportResult entry, logs a Warn line with the rejection reasons,
and publishes TrackImportFailedEvent. Real Transmission /
SABnzbd-originated imports also light up a BookImportIncomplete row
in Activity history via the existing
CompletedDownloadService.Process chain. Was previously a silent
Debug-level skip. (Cycle 7d.)
Restores the bulk "adopt what's already on disk" flow that every other
Servarr app ships (ImportSeries / ImportMovies / ImportArtist) and
that upstream Readarr dropped when it forked from Lidarr — leaving
RootFolders/UnmappedFolder.cs and
MediaFiles/BookImport/ImportArtistDefaults.cs behind as dead code.
src/NzbDrone.Core/RootFolders/RootFolderService.cs—GetUnmappedFolders()lists first-level subdirectories of a root folder that no author's path claims, minus a special-folder blocklist ($RECYCLE.BIN,lost+found,@eaDir, Calibre's.caltrash, …). TakesIAuthorRepositoryrather thanIAuthorService: the latter would close a DryIoc cycle throughAuthorPathBuilder→IRootFolderService. Surfaced asRootFolderResource.UnmappedFolders, andIgnored inTableMapping.cssince it is recomputed per read, never persisted.src/Readarr.Api.V1/Author/AuthorImportController.cs—POST /api/v1/author/import, bulk-adds viaIAddAuthorService.ImportAuthors(the outcome-reporting form of the pre-existingAddAuthors). Returns{ imported, skipped }: a row that repeats an earlier row's author, is already in the library, or cannot be resolved is skipped with a reason rather than failing the batch.frontend/src/LibraryImport/— the two-step UI, at/add/import. See that folder'sREADME.mdfor why lookups are queued rather than parallel (Open Library meters the search endpoint) and why no row is ever pre-selected.
Complements rather than replaces the root folder rescan button: rescan is file-driven and unattended, this is folder-driven and asks the user to confirm each author, which narrows the subsequent file matching from all of Open Library to that author's bibliography.
azure-pipelines.yml:22—majorVersion: '1.2.6-beta'.distribution/docker/Dockerfile— self-contained multi-stage build (Phase 9b). Compiles backend + frontend inside the image, runs onaspnet:10.0-alpine. Build command + run shape documented indistribution/docker/README.md.- GitHub Actions replaces Azure Pipelines as the CI driver.
- Librarr fork additions — what this fork added on top of inherited Readarr
- Overview
- High-level architecture
- Repository layout
- Backend (.NET) architecture
- Frontend (React) architecture
- Build, CI, packaging, standards
- Conventions & standards
- Contradictions, antipatterns, smells
- Open challenges
- How to find your way around
Readarr is an ebook and audiobook collection manager — the Servarr-family
sibling of Sonarr (TV), Radarr (movies), and Lidarr (music). It monitors RSS
feeds and indexers, downloads via Usenet/BitTorrent clients, applies quality
profiles, optionally integrates with Calibre, and renames/sorts files
(README.md:12-27).
The architecture is forked from Sonarr and shows it in two visible ways:
-
C# namespaces stay
NzbDrone.*while csproj/assembly names areReadarr.*. This split is set explicitly insrc/Directory.Build.props:97-99:<!-- For now keep the NzbDrone namespace --> <RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
So
src/NzbDrone.Core/Readarr.Core.csprojbuilds an assembly namedReadarr.Corewhose default namespace isNzbDrone.Core. Newer projects (Readarr.Api.V1,Readarr.Http) keep both name and namespace asReadarr.*. See §8. -
Stylecop.rulesetstill labels itself "Rules for Radarr" — line 1 literally:<RuleSet Name="Stylecop.ruleset" Description="Rules for Radarr" ToolsVersion="15.0">
The ruleset was forked from Radarr without retitling.
The product currently versions itself at 1.2.6-beta
(azure-pipelines.yml:22). The AssemblyVersion 10.0.0.* in
Directory.Build.props:77 is the historical Readarr placeholder the CI
overwrites at build time — not the shipping version, and not bumped
for this beta. Revisit at the 1.0.0 final cut.
┌────────────────────────────────────┐
Browser (React SPA) ───┤ Kestrel / ASP.NET Core 6 │
│ Readarr.Http (middleware) │
│ Readarr.Api.V1 (REST) │
│ NzbDrone.SignalR (real-time) │
└────────────────┬───────────────────┘
│ DryIoc DI
┌────────────────┴───────────────────┐
│ NzbDrone.Core (business logic) │
│ • Books / Author / Edition domain │
│ • Indexers + DownloadClients │
│ (ThingiProvider plugin model) │
│ • Messaging: Events + Commands │
│ • Jobs / Housekeeping / Scheduler │
│ • Decision engine (specs) │
│ • Parser (regex) │
│ • Datastore (custom Dapper ORM, │
│ FluentMigrator, SQLite|Postgres)│
│ • MediaFiles / Tags / Calibre │
│ • Notifications / Import Lists │
└────────────────┬───────────────────┘
│
┌────────────────┴───────────────────┐
│ NzbDrone.Common (utilities) │
│ Env, Disk, HTTP, Cache, Json, │
│ Instrumentation (NLog targets) │
└────────────────┬───────────────────┘
│
┌─────────────────┬──────────┴──────────┬────────────────┐
│ NzbDrone.Windows│ NzbDrone.Mono │ NzbDrone.Console│
│ (Win service + │ (Linux/macOS svc, │ (CLI entry) │
│ Firewall API) │ posix signals) │ │
└─────────────────┴──────────┬──────────┴────────────────┘
│
┌────────────────┴───────────────────┐
│ NzbDrone (Readarr.exe) │
│ Tray/Windows-Forms shim → Bootstrap│
└────────────────────────────────────┘
External: Indexers (Newznab/Torznab/Gazelle/FileList/Nyaa/IPTorrents),
Download clients (Sabnzbd/QBittorrent/Transmission/Deluge/rTorrent
/uTorrent/Aria2/NzbGet/NzbVortex/DownloadStation/Hadouken/Blackhole),
Notifications (Discord/Slack/Telegram/Email/Plex/Webhook/…),
Calibre Content Server, Goodreads/LazyLibrarian import sources.
Process modes (Bootstrap.cs:62-107):
• Service — Windows service
• Interactive — tray or console
• Utility — one-shot install/uninstall/register-URL
Default HTTP port 8787, HTTPS port 6868 (NzbDrone.Host/Bootstrap.cs:135-136).
Three configuration sources merged at startup: XML file
(config.xml), in-memory keys (data-protection path), env vars
(Bootstrap.cs:236-240).
Readarr-develop/
├── ARCHITECTURE.md ← this document
├── README.md Marketing/install page, links to wiki
├── CLA.md No CLA — GPL v3 inbound = outbound
├── CODE_OF_CONDUCT.md Contributor Covenant
├── CONTRIBUTING.md Librarr contributor guide (branch, build, PR handling)
├── SECURITY.md Vuln-report pointer → GitHub advisories
├── LICENSE.md GPL v3 (~35 KB full text)
├── azure-pipelines.yml ~1257-line multi-stage CI definition
├── build.sh Local-and-CI build orchestration (~442 lines)
├── test.sh dotnet test wrapper
├── docs.sh OpenAPI/swagger doc generation
├── package.json Root-level package.json (no nested one in frontend/)
├── tsconfig.json 3-line stub → frontend/tsconfig.json
├── yarn.lock Yarn classic lock
├── Logo/ SVG/PNG project artwork
├── distribution/
│ ├── windows/ InnoSetup installer assets + service helpers
│ └── osx/ .app bundle template + DMG scripts
├── schemas/
│ └── torznab.xsd Torznab indexer feed schema (consumed by
│ NzbDrone.Core/Indexers/Torznab,Newznab)
├── frontend/
│ ├── babel.config.js
│ ├── postcss.config.js
│ ├── tsconfig.json TS config for the SPA (used by webpack ts-loader)
│ ├── jsconfig.json
│ ├── typings/ Hand-written ambient .d.ts files
│ ├── .eslintrc.js 393-line ESLint config (see §6)
│ ├── .stylelintrc CSS lint rules
│ ├── .prettierrc.json Prettier config
│ ├── build/ Webpack 5 config, loaders, CSS pipeline
│ └── src/
│ ├── index.ts Polyfill entry
│ ├── bootstrap.tsx Real SPA bootstrap (history, store, render)
│ ├── App/ App shell + AppRoutes
│ ├── Author/ Author index / details / edit
│ ├── Book/ Book index / details / edit / delete
│ ├── BookFile/ File editor / table
│ ├── Calendar/ Calendar page
│ ├── Commands/ Server-command bridges
│ ├── Components/ Shared UI primitives (Page, Modal, Form, Table…)
│ ├── InteractiveSearch/
│ ├── Organize/ File rename previews
│ ├── Retag/ Tag rewrite previews
│ ├── Settings/ All settings pages (Indexers, Quality, UI, …)
│ ├── Store/ Redux store, actions, reducers, selectors
│ ├── Styles/ Globals, themes, CSS variables
│ ├── System/ System status, tasks, logs, backups
│ ├── Utilities/ Date/string/api helpers, createAjaxRequest
│ └── Wanted/ Missing / cutoff-unmet pages
└── src/
├── Readarr.sln The .NET solution
├── Readarr.sln.DotSettings ReSharper team settings (committed)
├── Directory.Build.props Root MSBuild props for every project
├── Directory.Build.targets Near-empty targets file (75 bytes)
├── Directory.Packages.props Central Package Management (real CPM, see §6)
├── NuGet.config Package sources + Servarr feed
├── stylecop.json StyleCop rule settings
├── Stylecop.ruleset StyleCop rule actions ("Rules for Radarr")
├── coverlet.runsettings Coverage collector config
├── postgres.runsettings Postgres-test connection settings
│
├── Libraries/ Vendored Win32 firewall interop DLL
├── Targets/ Custom MSBuild .targets (RID enumeration)
├── ServiceHelpers/ ServiceInstall/ServiceUninstall exe projects
│
├── NzbDrone/ Readarr.csproj — main entry exe (tray shim)
├── NzbDrone.Common/ Cross-cutting utilities (no business logic)
├── NzbDrone.Console/ Console-mode entry point exe
├── NzbDrone.Core/ THE business-logic project (largest)
├── NzbDrone.Host/ ASP.NET Core host: Bootstrap + Startup, DI
├── NzbDrone.SignalR/ SignalR Hub for real-time UI push
├── NzbDrone.Mono/ Linux/macOS platform shim (posix, service)
├── NzbDrone.Windows/ Windows platform shim (service, firewall)
├── NzbDrone.Update/ Self-updater executable
├── Readarr.Api.V1/ REST API v1 — controllers + resources
├── Readarr.Http/ HTTP middleware: auth, errors, framing
│
├── NzbDrone.Api.Test/ ← All 11 *.Test projects: NUnit + Moq
├── NzbDrone.Automation.Test/ Selenium-driven UI automation
├── NzbDrone.Common.Test/
├── NzbDrone.Core.Test/
├── NzbDrone.Host.Test/
├── NzbDrone.Integration.Test/ Full-stack web integration tests
├── NzbDrone.Libraries.Test/
├── NzbDrone.Mono.Test/
├── NzbDrone.Test.Common/ Shared test infrastructure (not actually a test
│ project — it's a fixture library)
├── NzbDrone.Test.Dummy/ Tiny dummy assembly used by tests
├── NzbDrone.Update.Test/
└── NzbDrone.Windows.Test/
Top-level dirs/files have their own per-directory README.md for orientation;
this document is the canonical overview.
29 csproj projects total (15 production + 11 tests + 2 service-helper exes +
1 dummy). Counts derived from grep -E "^Project\(" src/Readarr.sln.
Production (15):
| Project (folder) | csproj | Output | Purpose |
|---|---|---|---|
NzbDrone |
Readarr.csproj |
exe (tray) | Tray/Windows entry point — wraps Bootstrap.Start |
NzbDrone.Console |
Readarr.Console.csproj |
exe (console) | Console entry point — non-tray launcher |
NzbDrone.Common |
Readarr.Common.csproj |
library | Disk, env, http, cache, json, instrumentation utilities |
NzbDrone.Core |
Readarr.Core.csproj |
library | Business logic — domain, ORM, providers, jobs, messaging |
NzbDrone.Host |
Readarr.Host.csproj |
library | Bootstrap, Startup, DI wiring, Kestrel + URL setup |
NzbDrone.SignalR |
Readarr.SignalR.csproj |
library | MessageHub for real-time push to UI |
NzbDrone.Mono |
Readarr.Mono.csproj |
library | Linux/macOS platform shim (signals, service hosting) |
NzbDrone.Windows |
Readarr.Windows.csproj |
library | Windows platform shim (service, firewall — Libraries/Interop.NetFwTypeLib.dll) |
NzbDrone.Update |
Readarr.Update.csproj |
exe (update) | Self-updater binary that replaces files |
Readarr.Api.V1 |
Readarr.Api.V1.csproj |
library | REST controllers and *Resource DTOs |
Readarr.Http |
Readarr.Http.csproj |
library | HTTP middleware: auth, error handling, CORS, framing |
ServiceHelpers/ServiceInstall |
ServiceInstall.csproj |
exe | Windows service install helper |
ServiceHelpers/ServiceUninstall |
ServiceUninstall.csproj |
exe | Windows service uninstall helper |
Tests (11): all named *.Test. They use NUnit 3.14.0 + Moq 4.17.2
(src/Directory.Packages.props:29,38) plus optional FluentAssertions, NBuilder,
AutoFixture. NzbDrone.Test.Common is a test fixture library (utilities)
rather than a test runner project.
Build infrastructure (not csproj):
| Dir | What |
|---|---|
src/Targets/ |
Custom MSBuild .targets (RID enumeration) |
src/Libraries/ |
Vendored Interop.NetFwTypeLib.dll (Win firewall API) |
src/Directory.Build.props |
Common props applied to every project |
src/Directory.Build.targets |
Near-empty — placeholder |
src/Directory.Packages.props |
Central package versions (CPM) |
src/stylecop.json |
StyleCop settings |
src/Stylecop.ruleset |
StyleCop rule actions |
src/coverlet.runsettings |
Coverage settings |
src/postgres.runsettings |
Postgres-test connection |
NzbDrone (exe) & NzbDrone.Console (exe)
│ references:
▼
NzbDrone.Host ────────────────────────────► Readarr.Http
│ ──► NzbDrone.SignalR │
│ ──► Readarr.Api.V1 ────────────┤
│ │
▼ ▼
NzbDrone.Core ◄────────────────────────────────
│
▼
NzbDrone.Common
▲
NzbDrone.Windows / NzbDrone.Mono ───────────────► NzbDrone.Common
(platform shims, used by Host)
Test projects depend on the corresponding production project plus
NzbDrone.Test.Common. The dependency graph is strictly bottom-up —
NzbDrone.Common has no dependency on NzbDrone.Core, and NzbDrone.Core
has no dependency on Readarr.Http or Readarr.Api.V1. The HTTP/API layer
talks to Core, not vice-versa.
The DI container is DryIoc 5.4.3 (src/Directory.Packages.props:7-8),
bridged into ASP.NET Core via DryIoc.Microsoft.DependencyInjection. Setup
lives in src/NzbDrone.Host/Bootstrap.cs:
// Bootstrap.cs:90 — utility mode
.UseServiceProviderFactory(new DryIocServiceProviderFactory(
new Container(rules => rules.WithNzbDroneRules())))
// Bootstrap.cs:150 — host mode (Kestrel)
.UseServiceProviderFactory(new DryIocServiceProviderFactory(
new Container(rules => rules.WithNzbDroneRules())))The WithNzbDroneRules() extension and AutoAddServices registration scan a
fixed list of assemblies (Bootstrap.cs:37-44):
public static readonly List<string> ASSEMBLIES = new List<string>
{
"Readarr.Host", "Readarr.Core", "Readarr.SignalR",
"Readarr.Api.V1", "Readarr.Http"
};Auto-registration: convention-based scan of the listed assemblies, registering every concrete public class against its implemented interfaces. Most lifetimes are singletons; controllers and per-request services are scoped via the ASP.NET Core bridge.
There is no Entity Framework, no Marr.Data, and no raw Dapper-as-the-API.
Instead, NzbDrone.Core/Datastore/ is a hand-rolled mini-ORM on top of
Dapper 2.0.151 (src/Directory.Packages.props:6). Core pieces:
Datastore/BasicRepository.cs— genericBasicRepository<TModel>withInsert,Update,Delete,Find,All,Single, joins, paged queries.Datastore/SqlBuilder.cs— fluent SQL builder.Datastore/WhereBuilderSqlite.cs/WhereBuilderPostgres.cs— dialect- specific predicate translation.Datastore/DbFactory.cs+Datastore/ConnectionStringFactory.cs— picks SQLite (default) or Postgres based onconfig.xml/Readarr:Postgresenv vars (Bootstrap.cs:102,161).Datastore/MigrationController.cs— runs migrations on startup using a Servarr-forked FluentMigrator:Servarr.FluentMigrator.Runner3.3.2.9 (Directory.Packages.props:12-14).- Migration classes live in
Datastore/Migration/0xx_*.cs, numbered. - There are 47 migrations as of 2026-07-30 (latest
046_author_audiobook_quality_profile) — a working schema-evolution discipline.
- SQLite uses a forked
System.Data.SQLite.Core.Servarrbuild (Directory.Packages.props:52) — necessary because the official System.Data.SQLite package doesn't support the platforms Servarr ships.
JSON column converters for complex types (CustomFormat, EmbeddedDocument,
Quality, TimeSpan, etc.) live under Datastore/Converters/. Models are
plain POCOs that inherit from ModelBase (with Id property).
The API project Readarr.Api.V1 exposes REST endpoints using ASP.NET Core
MVC (not Nancy — older Servarr code used Nancy, that's gone). Pattern:
{Entity}Controller : RestController<{Entity}Resource>(e.g.Readarr.Api.V1/Books/BookController.cs).{Entity}Resource— DTO living next to the controller.- Manual mapping (no AutoMapper) between domain models and resources.
- Some controllers extend a SignalR-aware base
(
{Entity}ControllerWithSignalR<TResource>), which broadcasts CRUD events.
Readarr.Http provides:
- API-key authentication (
AuthenticationBuilderExtensions.cs). - Error-to-HTTP exception filters.
- CORS, response framing, request logging.
- Static-content serving for the SPA.
Swagger schema is generated by Swashbuckle.AspNetCore.SwaggerGen 6.5.0
(Directory.Packages.props:49). The generated openapi.json is intentionally
excluded from CI triggers (azure-pipelines.yml:33,43) so committing a
regenerated spec doesn't kick off a build loop.
NzbDrone.SignalR/MessageHub is the single hub. Controllers that need to push
updates inherit *WithSignalR base classes; events from the in-process
EventAggregator are forwarded to all connected clients. Used for:
- Command progress (
CommandUpdated). - Queue updates.
- Health-check changes.
- Entity CRUD (author/book added/deleted/updated).
Frontend consumes via @microsoft/signalr (package.json:33).
The most distinctive backend pattern. Indexers, Download Clients,
Notifications, Import Lists, and Metadata sources all share one plugin
abstraction rooted in NzbDrone.Core/ThingiProvider/:
IProvider— marker interface.ProviderFactory<TProvider, TDefinition>— discovers, instantiates, and caches providers.ProviderStatusServiceBase— tracks health with exponential back-off when a provider fails repeatedly.IProviderRepository<TDefinition>— stores per-instance config (settings, URL, API key, etc.) in the database.
Concrete provider hierarchies built on this:
| Domain | Folder | Base classes |
|---|---|---|
| Indexers | NzbDrone.Core/Indexers/ |
IndexerBase → HttpIndexerBase → concrete |
| Download clients | NzbDrone.Core/Download/Clients/ |
DownloadClientBase → TorrentClientBase / UsenetClientBase → concrete |
| Notifications | NzbDrone.Core/Notifications/ |
NotificationBase → concrete (Discord/Slack/Email/…) |
| Import lists | NzbDrone.Core/ImportLists/ |
ImportListBase → HttpImportListBase → concrete |
| Metadata sources | NzbDrone.Core/MetadataSource/ |
per-source (e.g., BookInfoProxy) |
Concrete indexers: Newznab, Torznab, Gazelle, FileList, Nyaa, IPTorrents, TorrentRss. Concrete download clients: Sabnzbd, NzbGet, NzbVortex, QBittorrent, Transmission, Deluge, rTorrent, uTorrent, Aria2, DownloadStation (torrent + usenet), Hadouken, Blackhole (torrent + usenet). The base hierarchy is deep — see §8 for the duplication smell.
Two in-process channels, both in NzbDrone.Core/Messaging/:
Events/—EventAggregatorsynchronous pub/sub. Anything implementingIHandle<TEvent>(orIHandleAsync<TEvent>) receives matching events. Handlers live next to the domain that owns them (e.g.,Books/Events/Handlers/AuthorAddedHandler.cs).Commands/—CommandQueueManager+CommandExecutorrunICommands on a background queue. Handlers implementIExecute<TCommand>. Commands surface to the user via the/api/v1/commandendpoint and SignalRCommandUpdatedpushes.
Scheduling: NzbDrone.Core/Jobs/ — TaskManager, Scheduler, ScheduledTask.
A timer enqueues commands at configured intervals (RSS sync, refresh
metadata, backup, housekeeping, etc.). Cleanup runs come from
NzbDrone.Core/Housekeeping/ (~20 housekeeper classes).
Configuration splits across two stores:
config.xml— bootstrap config (Bootstrap.cs:237):BindAddress,Port,SslPort,SslCertPath,ApiKey, etc. Managed byNzbDrone.Core/Configuration/ConfigFileProvider.cs.- Database
Configtable — key/value JSON store for runtime app preferences. Accessed viaConfigServiceandConfigRepository.
Postgres-specific options bind from a Readarr:Postgres configuration section
(Bootstrap.cs:102,161).
NLog 5.1.4 (Directory.Packages.props:34). Targets:
- Console / file (rotated).
DatabaseTarget— writes structured log rows to the database for the Logs page in the UI.- Sentry sink (Sentry 4.0.2).
- Syslog target (
NLog.Targets.Syslog 7.0.0).
ReconfigureLogging allows the UI to change log level at runtime (in
NzbDrone.Common/Instrumentation/).
NzbDrone.Core/DecisionEngine/ decides whether a release should be grabbed.
Each rule is an ISpecification<RemoteBook> (or RemoteAuthor). The
DownloadDecisionMaker runs every spec and produces an accept/reject result
with rejection reasons. Specs are auto-registered by the DI scan.
This pattern also drives the import pipeline
(NzbDrone.Core/MediaFiles/BookImport/Specifications/).
NzbDrone.Core/Parser/Parser.cs is ~905 lines of regular expressions for
extracting author/title/year/quality from release names. Companion files:
QualityParser.cs, LanguageParser.cs, IsoLanguages.cs. File-format tag
readers live in NzbDrone.Core/MediaFiles/: EpubTag.cs, AzwTag.cs,
AudioTag.cs (~553 lines, wraps TagLibSharp-Lidarr 2.2.0.19).
| Project | Style |
|---|---|
*.Test |
NUnit unit tests, one project mirrors each production project |
NzbDrone.Test.Common |
Shared TestBase<T>, mock builders, fixture helpers |
NzbDrone.Integration.Test |
Spins up the full host and exercises the REST API + DB |
NzbDrone.Automation.Test |
Selenium WebDriver (Chrome) browser tests — pinned to Selenium 3.141.0 and ChromeDriver 91.0.4472.x (Directory.Packages.props:43-44) — both several major versions behind current |
coverlet.collector is the coverage tool (Directory.Packages.props:5),
configured via src/coverlet.runsettings. Postgres-mode runs use
src/postgres.runsettings.
A React 18 + Redux 4 SPA, JavaScript-first with a barely-started
TypeScript migration. All metadata is in the root package.json (no
nested frontend/package.json exists). Versions verified against
package.json on 2026-07-30:
| Concern | Package & version |
|---|---|
| Framework | react 18.3.1, react-dom 18.3.1 — mounted with createRoot (frontend/src/bootstrap.tsx:3,18), not legacy ReactDOM.render |
| Router | react-router 5.2.0, react-router-dom 5.2.0, history 4.10.1 |
| Redux | redux 4.2.1, react-redux 7.2.4, redux-actions 2.6.5, redux-thunk 2.4.2, redux-batched-actions 0.5.0, redux-localstorage 0.4.1, reselect 4.1.8, connected-react-router 6.9.3 |
| HTTP | jquery 3.7.1 (only inside Utilities/createAjaxRequest.js) |
Two caveats that are easy to get wrong:
- React 18 is real, but the ecosystem around it is not caught up.
react-reduxis still 7.2.4, which predates React 18 and does not useuseSyncExternalStore. Combined with legacycreateStoreandconnected-react-router6.x, the app is on React 18 without any of the concurrent features. Upgrading React was the easy half. - The TypeScript migration is ~6% done, not ~29%. Any count of
.tsfiles that does not exclude*.css.d.tsis wrong by an order of magnitude: there are 353 generated CSS declaration files against just 32 hand-written.tsand 36.tsx, versus 1004.js. | Realtime |@microsoft/signalr 6.0.25| | Types |typescript 5.1.6,prop-types 15.8.1— coexist | | Icons |@fortawesome/*(free) | | DnD |react-dnd 14.0.4(+ HTML5/touch backends) | | Misc UI |react-tabs,react-popper,react-autosuggest,react-virtualized,react-custom-scrollbars-2,react-document-title,react-lazyload,react-measure,react-slider,mousetrap,clipboard,fuse.js,filesize,moment| | Lodash |lodash 4.17.21— used heavily across actions and reducers | | Error report |@sentry/browser 7.51.2,@sentry/integrations 7.51.2| | Build |webpack 5.88.2,babel-loader 9.1.3,postcss 8.4.38,ts-loader 9.4.4,mini-css-extract-plugin,terser-webpack-plugin,css-modules-typescript-loader,typescript-plugin-css-modules| | Lint/format |eslint 8.57.0,eslint-plugin-react 7.34.1,eslint-plugin-react-hooks 4.6.0,eslint-plugin-prettier 4.2.1,prettier 2.8.8,stylelint 15.10.3| | Runtime pin | Node 24.18.1 LTS, declared three ways that must agree:.nvmrc,package.jsonengines+volta, andNODE_VERSIONin both GitHub workflows. Node 20 went EOL 2026-04-30 |
There is no test runner declared in package.json — the frontend has
no actively running JS test suite despite some .test.js-style files
scattered around. (Servarr frontend tests run via ESLint/Stylelint rules and
the Selenium automation suite on the backend side.)
frontend/src/
├── index.ts Polyfill entry; imports bootstrap.tsx
├── bootstrap.tsx SPA bootstrap — history, store, <App /> render
├── polyfills.js Array/Object polyfills for older browsers
├── App/
│ ├── App.js Top-level <Provider> + <ConnectedRouter>
│ ├── AppRoutes.js Static route table (no lazy/code-splitting)
│ ├── ColorImpairedContext.js Accessibility colour-mode context
│ └── …
├── Author/ Author index / details / edit / delete pages
├── Book/ Book index / details / edit / delete pages
├── BookFile/ File editor / table
├── Calendar/ Calendar page
├── AddNewItem/ "Add new" flows
├── AddSeries/ (legacy naming — leftover from Sonarr)
├── Activity/ Activity / queue / history / blocklist
├── Commands/ Server-command bridges
├── Components/ Shared UI primitives
│ ├── Form/ Form inputs (TagInput, …)
│ ├── Link/ Icon buttons, nav links
│ ├── Loading/ Spinners
│ ├── Menu/ Dropdowns
│ ├── Modal/ Modals, confirm, edit
│ ├── Page/ Page, PageHeader, PageSidebar, PageToolbar
│ ├── Router/ Switch wrapper
│ ├── Table/ VirtualTable
│ ├── Swipe/ Touch swipe headers
│ └── SignalRConnector.js Single SignalR client → Redux dispatcher
├── InteractiveSearch/ Manual search UI
├── Organize/ Rename previews
├── Retag/ Retagging previews
├── Settings/ Settings pages (Indexers, Quality, Profiles, UI, …)
├── Store/ Redux store, actions, reducers, selectors, thunks
│ ├── Actions/ Action creators by feature
│ ├── Actions/Creators/ Reducer/handler factories
│ ├── Middleware/ appMiddleware, sentryMiddleware, …
│ ├── Migrators/ Local-storage schema migrators
│ ├── Reducers/ Combined root reducer
│ ├── Selectors/ Reselect selectors
│ ├── createAppStore.js Store factory (legacy createStore)
│ └── thunks.js Custom createThunk/handleThunks pattern
├── Styles/
│ ├── globals.css
│ ├── scaffolding.css
│ ├── Themes/ dark.js / light.js — colour tokens
│ └── Variables/ CSS variables for colors, fonts, mixins
├── System/ System status, tasks, logs, backups
├── Utilities/
│ ├── Api/
│ ├── Date/
│ ├── String/ translate (custom i18n shim)
│ ├── Object/
│ └── createAjaxRequest.js jQuery $.ajax wrapper
├── Wanted/ Missing / cutoff-unmet pages
└── typings/ (sibling under frontend/, not under src/) ambient .d.ts
-
PascalCase folder per component. A component, its CSS module, and its Redux container live together:
Foo/Foo.js+Foo/Foo.css+Foo/FooConnector.js. -
Class components dominate. Most pages and complex components are class components with
constructor/componentDidMount/componentWillUnmount. Newer code uses hooks — there are 136 hook callsites across just 28 files (useState/useEffect/useCallback/useMemo/…) tree-wide, so adoption has started but is partial. -
PropTypes for
.js, TS types for.ts/.tsx. ESLint enforcesreact/prop-types: 2only on the JS side and turns itoffon TS files (frontend/.eslintrc.js:317,365). Both worlds coexist by design while the migration is in progress. -
File extensions are a migration signal — but only if you exclude the generated ones. Counts of
frontend/src/**on 2026-07-30:.js: 1004.jsx: 0 (everyone uses.jswith JSX inside).ts: 32 hand-written, plus 353 generated*.css.d.ts.tsx: 36
~6% TS by file count, not the ~29% previously recorded here. The old figure counted the CSS Modules declaration files — which are emitted by the build, not written by anyone — as migrated TypeScript, making the migration look five times further along than it is.
Redux store assembled the legacy way — createStore from redux, not
configureStore from RTK (Store/createAppStore.js). Action creator pattern:
// Store/Actions/authorActions.js
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';Two thunk styles coexist:
- The real
redux-thunkmiddleware (declared inpackage.json:84). - A custom
createThunk/handleThunkspair underStore/thunks.js— it's the predominant pattern. Action creators register named thunks; a middleware dispatches them when matching action types fire.
Reducer boilerplate is factored into generator functions under
Store/Actions/Creators/:
createFetchHandler.js—isFetching/isPopulated/error/itemsshape from a network fetch.createHandleActions.js— maps action types to reducer fragments.createSetReducerValueReducer.js— generic setter.
Selectors use reselect; deep-equality selectors via
createDeepEqualSelector.js.
Utilities/createAjaxRequest.jswraps jQuery$.ajaxas the sole HTTP client. Adds API-key header, content-type, request ID, error shaping. Nofetch/axios.Components/SignalRConnector.jsestablishes the SignalR connection, subscribes to entity channels, and dispatches Redux actions directly when events arrive. It is mounted once near the root and lives for the SPA's lifetime.
Static route table in App/AppRoutes.js — every page is imported at module
load (no lazy import / no code splitting). React Router 5 with
<ConnectedRouter> from connected-react-router so router state lives in
Redux. No client-side auth guards — the backend rejects unauthenticated
requests via the Readarr.Http middleware.
- Themes are JS objects with colour tokens (
Styles/Themes/dark.js,Styles/Themes/light.js) applied via CSS variables defined inStyles/Variables/. The user picks a theme in Settings → UI; it is persisted viaredux-localstorage. - No real i18n library. Translations are loaded from the backend and
resolved via
Utilities/String/translate.js. Strings live insrc/NzbDrone.Core/Localization/Core/*.json(excluded from CI triggers — seeazure-pipelines.yml:42).
Webpack 5 config under frontend/build/:
- Babel transpiles JS/TS (
@babel/preset-env,@babel/preset-react,@babel/preset-typescript). - PostCSS pipeline (
postcss-cssnext-style features via individual plugins:postcss-mixins,postcss-nested,postcss-simple-vars,postcss-color-function,autoprefixer). ts-loader+fork-ts-checker-webpack-pluginfor the TS files.css-modules-typescript-loader+typescript-plugin-css-modulesgenerate ambient.css.d.tsfor CSS Module class names — that's the only reason ESLint disablesprettier/prettierfor*.css.d.ts(frontend/.eslintrc.js:388).- Output goes to
./_output/UI(package.json:8).
Run with yarn start (watch mode) or yarn build. The .NET build invokes
yarn build as a step in build.sh.
azure-pipelines.yml (~1257 lines) defines stages and a matrix:
Setup → Build_Backend (Linux | Mac | Windows matrix)
→ Build_Frontend
→ Unit_Test
→ Integration_Test (incl. FreeBSD via cross-publish)
→ Automation_Test (Selenium on Chrome)
→ Package (10 RIDs + InnoSetup .exe + .dmg)
→ Sign (Authenticode for Windows)
→ Release (publish to internal feeds + GitHub Releases)
Notable variables (azure-pipelines.yml:6-23):
majorVersion: '1.2.6-beta'— the real shipping version (azure-pipelines.yml:22).minorVersion: $[counter('minorVersion', 1)]— auto-incremented.dotnetVersion: '10.0.302'— .NET 10 LTS (migrated 2026-07-30).nodeVersion: '24.X'— frontend runtime (Node 24 LTS).innoVersion: '6.2.0'— Windows installer toolchain.- VM images:
windows-2022,ubuntu-20.04,macOS-11— the macOS-11 image is deprecated by Azure DevOps; flag for maintenance.
Triggers:
developandmasterbranches (azure-pipelines.yml:25-29).- PRs to
developonly (azure-pipelines.yml:35-43). - Excluded paths:
.github,src/NzbDrone.Core/Localization/Core(Weblate- generated translations),src/Readarr.Api.*/openapi.json(generated swagger).
Subtle contradiction: StyleCop analysis only runs on the Linux job
(enableAnalysis: 'true' at azure-pipelines.yml:79; 'false' for Mac and
Windows on lines 83, 87). So even though EnforceCodeStyleInBuild=true is set
globally in Directory.Build.props:5, two of the three matrix legs ignore
StyleCop output. Style violations break CI only on Linux.
build.sh(~442 lines) — drives the full backend + frontend build for one or many RIDs. Modes via flags:--backend,--frontend,--packages,--enable-extra-platforms(addsfreebsd-x64andlinux-x86to the bundled RIDs via asedpatch on the .NET SDK'sMicrosoft.NETCoreSdk.BundledVersions.props, seeazure-pipelines.yml:102-111).test.sh— wrapsdotnet testwith the right runsettings.docs.sh— regenerates the OpenAPI/swagger.json artifact via the Swashbuckle CLI.
From src/Directory.Build.props:
TreatWarningsAsErrors=true(l.4) — strict.EnforceCodeStyleInBuild=true(l.5) — strict, but see the CI-matrix caveat above.ManagePackageVersionsCentrally=true(l.7) — real Central Package Management is on.Directory.Packages.propsuses proper<PackageVersion>elements (e.g.,<PackageVersion Include="DryIoc.dll" Version="5.4.3" />).<RuntimeIdentifiers>(l.11) lists 10 RIDs:win-x64,win-x86,osx-x64,osx-arm64,linux-x64,linux-musl-x64,linux-arm,linux-musl-arm,linux-arm64,linux-musl-arm64. Note: nowin-arm64despite the build exploring "Windows ARM64" — that platform is not currently produced.AssemblyVersion=10.0.0.*(l.77) — placeholder; CI overrides.GenerateDocumentationFile=true+NoWarn=CS1591(l.35-39) — XML doc file is produced, but the "missing XML doc on public member" warning is suppressed wholesale. The XML file is therefore mostly empty descriptions.RootNamespaceremap to preserveNzbDrone.*(l.97-99).- StyleCop wired up centrally with a guard for
EnableAnalyzers(l.111-119) — so CI can disable per matrix leg.
Package list (highlights from Directory.Packages.props):
- DryIoc 5.4.3 (DI).
- Dapper 2.0.151.
- Servarr-forked FluentMigrator 3.3.2.9 (SQLite + Postgres runners).
- Servarr-forked
System.Data.SQLite.Core.Servarr 1.0.115.5-18. - Servarr-forked
Mono.Posix.NETStandard 5.20.1.34-servarr22. - Lidarr-forked
TagLibSharp-Lidarr 2.2.0.19. - NLog 5.1.4, NLog.Targets.Syslog 7.0.0.
- Newtonsoft.Json 13.0.3 + System.Text.Json 6.0.9 — both present.
- RestSharp 106.15.0 — last major before RestSharp 107's breaking rewrite; pinned to the legacy major.
- Polly 8.3.1.
- FluentValidation 9.5.4.
- SixLabors.ImageSharp 3.1.4.
- Sentry 4.0.2 (.NET).
- Selenium 3.141.0 + ChromeDriver 91.0.4472.10100 — several years old.
- Windows:
distribution/windows/setup/contains InnoSetup script(s); CI runs InnoSetup 6.2.0 (azure-pipelines.yml:20) to produce the installer.exe. Windows builds are Authenticode-signed. - macOS:
distribution/osx/contains the.apptemplate and DMG creation scripts. Both Intel (osx-x64) and Apple Silicon (osx-arm64) builds. - Linux: ships as
.tar.gzarchives per RID — no.deb/.rpm.
StyleCop is wired in via analyzer NuGet
(Directory.Build.props:111-119). src/Stylecop.ruleset (115 lines) sets
actions per rule. Highlights:
- SA1101 (
PrefixLocalCallsWithThis) →None. Inverted via SX1101 (Do not prefix local members with this) →Warning(l.112) — the project actively bansthis.prefix on instance members. - Documentation rules SA1600–SA1652 are all
None(l.55-107) — public APIs are not required to have XML doc comments. Pairs with theNoWarn=CS1591setting and ironicallyGenerateDocumentationFile=true. - Naming rules SA1300, SA1301, SA1303, SA1304, SA1306, SA1309, SA1310 →
None(l.26-32) — allows non-standard naming like underscore-prefixed fields (also encoded by SX1309_for instance fields). - Maintainability rule SA1402 (
FileMayOnlyContainASingleType) →None(l.38).
src/stylecop.json enforces:
- 4-space indentation (no tabs).
- Newline required at end of file.
usingdirectives outside namespace, system usings first.
EditorConfig at the repo root applies cross-language formatting (final newline, trim trailing whitespace, indent style/size).
Readarr.sln.DotSettings ships team ReSharper conventions in-repo.
Frontend lint (frontend/.eslintrc.js, 393 lines):
react/prop-types: 2for.js,offfor.ts/.tsx(l.317, 365).react-hooks/rules-of-hooks: 'error'andreact-hooks/exhaustive-deps: 'error'(l.322-323) — hooks usage is fully linted.prettier/prettier: 'error'for.ts/.tsx(l.366); onlyofffor generated*.css.d.ts(l.388). The TS migration brings Prettier with it.simple-import-sort,filenames/match-exported, plus a long house style (single quotes, semicolons, 2-space indent, no nested ternaries, max depth 5).
No husky / lint-staged / pre-commit hooks — lint runs in CI only.
NUnit 3.14.0 + Moq 4.17.2 across all *.Test projects; FluentAssertions
5.10.3, NBuilder 6.1.0, AutoFixture 4.17.0 in selected ones.
coverlet.collector is the coverage driver, configured by
src/coverlet.runsettings (no enforced threshold).
src/postgres.runsettings swaps the connection string so the same tests can
exercise Postgres.
NzbDrone.Automation.Test uses Selenium WebDriver against headless Chrome —
ChromeDriver 91 pins the Chrome major version, and Selenium 3.141 has been
unsupported upstream since the 4.x line. This is the only frontend-touching
test surface in the repo.
| File | Summary |
|---|---|
LICENSE.md |
GPL v3 (full text, ~35 KB) |
CLA.md |
States there is no CLA — GPL v3 inbound = outbound. The fork dropped upstream's |
CODE_OF_CONDUCT.md |
Contributor Covenant. Enforcement contact is the maintainer via private advisory, not upstream's dead mailbox |
CONTRIBUTING.md |
Librarr-specific: branch off main, full ./build.sh, and what happens when a PR is landed by cherry-pick |
SECURITY.md |
Vuln-report pointer — GitHub advisories, not the retired Servarr channel |
README.md |
Opens with the current release banner, then install / migration guidance |
schemas/torznab.xsd |
Torznab feed schema consumed by Newznab/Torznab indexer code |
Backend naming. Per-domain, the file layout is consistent across NzbDrone.Core:
NzbDrone.Core/{Domain}/
├── Model/ Domain POCOs (Book.cs, Author.cs, Edition.cs)
├── {Entity}Repository.cs Persistence
├── {Entity}Service.cs Application service / facade
├── {Entity}Cache.cs Optional in-memory caches
├── Commands/ *Command, ICommand impls
├── Events/ *Event, IEvent impls
│ └── Handlers/ IHandle<*Event> classes
└── …
Readarr.Api.V1/{Domain}/:
Readarr.Api.V1/{Domain}/
├── {Entity}Controller.cs REST controller
├── {Entity}Resource.cs DTO
└── …
Common suffixes: Service, Repository, Controller, Resource,
Command, Event, Handler, Specification, Proxy, Provider,
Definition, Factory.
Frontend naming. PascalCase folder per component; CSS module imported as
styles; redux container suffixed Connector (e.g.,
AuthorIndexConnector.js). Action types use SCREAMING_SNAKE_CASE.
Style enforcement.
- C#: StyleCop analyzers run as part of build;
TreatWarningsAsErrors=truefails compilation on any warning. The CI matrix gates analysis only on the Linux leg, so Windows/Mac builds will tolerate style violations. - Frontend: ESLint + Stylelint runnable via
yarn lint/yarn stylelint-*scripts (package.json:11-14). Not gated by a pre-commit hook.
Threading.
- Backend command queue uses a single background worker thread per command
type;
EventAggregatordispatches events synchronously on the publisher thread by default (async handlers opt in viaIHandleAsync). - ASP.NET Core controllers are async/await throughout; Kestrel
AllowSynchronousIO = falseenforces it (Bootstrap.cs:179).
Cited so future cleanup can be precise.
NzbDrone.*namespaces ↔Readarr.*assemblies / csproj. Set deliberately via theRootNamespacerewrite atsrc/Directory.Build.props:97-99. Newcomers will be confused;using Readarr.Core;won't compile, butusing NzbDrone.Core;will.Stylecop.ruleset:1describes itself as "Rules for Radarr" — the ruleset was forked from Radarr without retitling.- Bootstrap assembly list mixes new + old names
(
Bootstrap.cs:37-44): all entries areReadarr.*, but the namespace they're discovered into isNzbDrone.Host. - Folder names use
NzbDrone.*while the project elements inside areReadarr.*.csproj(e.g.src/NzbDrone.Core/Readarr.Core.csproj).
- JS ↔ TS coexist. ~985
.jsfiles vs ~375.ts+ 36.tsx. PropTypes is enforced on the JS side (eslintrc.js:317) and turned off on the TS side (eslintrc.js:365). Some files mix both. - Class ↔ hooks coexist. 136 hook callsites across 28 files, alongside a much larger
fleet of class components.
react-hooksis fully linted, so new code is expected to use hooks. - Two thunk patterns coexist.
redux-thunk 2.3.0is declared inpackage.json:84, but the dominant pattern isStore/thunks.js's customcreateThunk/handleThunks. Adding a new feature requires picking one. @types/react 18.2.79and@types/react-dom 18.2.25inpackage.json:37-38, but actualreactandreact-domare pinned to17.0.2. The types describe a React version newer than what's installed.Volta vs CI Node mismatch.Resolved 2026-08-01. Volta,.nvmrc,enginesand both workflows now all say Node 24. The divergence this warned about was real and had already bitten once:@testing-library/jest-dom6.10.0 requires Node >=22, installed cleanly on a developer's newer Node, and took down three CI jobs on Node 20 (aef3e1f).
- StyleCop only on Linux CI.
azure-pipelines.yml:79,83,87setsenableAnalysis: truefor Linux,falsefor Mac and Windows. Two thirds of the matrix tolerate style violations. GenerateDocumentationFile=true+NoWarn=CS1591+SA1600/SA1601 →None. XML doc files are produced but missing-doc warnings are suppressed and the SA rules requiring docs are off — so the generated XML is mostly empty.- No pre-commit hooks. No
husky,lint-staged, orpre-commitmanifest in the tree. Lint runs only in CI. - No coverage thresholds.
coverlet.runsettingshas no fail-under setting — coverage can drift unmonitored.
The ThingiProvider plugin model is good in spirit, but the concrete
hierarchies suffer from:
- Deep inheritance —
IIndexer→IndexerBase→HttpIndexerBase→TorznabIndexerBase→Torznab(specific). Same shape forDownloadClientBase→TorrentClientBase→QBittorrent. - Copy-paste proxies. Download clients each have a "proxy" class
(
QBittorrentProxy,SabnzbdProxy,TransmissionProxy, …) that reimplements the same RestSharp request → response → exception-wrap loop. - Largest provider files (line counts approximate):
Download/Clients/QBittorrent/QBittorrent.cs~725 LoC.Download/Clients/Sabnzbd/Sabnzbd.cs~539 LoC.Download/Clients/DownloadStation/TorrentDownloadStation.cs~459 LoC.
| File | Approx LoC |
|---|---|
NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs |
~993 |
NzbDrone.Core/Parser/Parser.cs |
~905 |
NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs |
~725 |
NzbDrone.Core/MediaFiles/Calibre/CalibreProxy.cs |
~682 |
NzbDrone.Core/MediaFiles/BookImport/ImportApprovedBooks.cs |
~575 |
NzbDrone.Core/MediaFiles/MediaFileService/FileNameBuilder.cs |
~578 |
NzbDrone.Core/MediaFiles/AudioTag.cs |
~553 |
frontend/src/Book/Index/BookIndex.js |
~500 |
frontend/src/Author/Index/AuthorIndex.js |
~500 |
Parser.cs is the highest fragility risk — a single regex change can break
many release-name patterns at once.
- jQuery in a 2024-vintage React app.
Utilities/createAjaxRequest.jsis the only consumer, so it's containable, but jQuery is in every bundle. - Selenium 3.141.0 + ChromeDriver 91 for the UI automation suite. Chrome 91 is years old and Selenium 4 has been the supported line since 2021. The automation suite is therefore brittle and divergent from any current Chrome install.
- Vendored Win32 interop DLL.
src/Libraries/Interop.NetFwTypeLib.dllis committed as a binary because the only way to call the Windows Firewall COM API from .NET is via this interop assembly. Provenance is opaque from a pure source-only audit. Newtonsoft.JsonandSystem.Text.Jsonboth ship. Migration to STJ is partial; many model converters still targetJsonConverterfrom Newtonsoft.ImplicitUsings/Nullableare not enabled inDirectory.Build.props. Pre-net6 idioms — extrausinglines and no null-safety annotations. Enabling them would require a sweep but would catch real bugs (especially in the messaging and provider layers wherenullflows freely).- macOS-11 CI image is on Azure's deprecation list (per their hosted-agent lifecycle). Builds will start failing when the image is removed.
win-arm64is not in the RID list even though Windows on ARM has been shipping since 2021. Worth a note for Surface Pro X / WSL2-ARM users.
tsconfig.jsonat the repo root is a 3-line stub that justextendsfrontend/tsconfig.json. Harmless, but easy to mistake for the real config.Directory.Build.targets(75 bytes) is essentially empty — its presence suggests intent that was never followed through.CONTRIBUTING.md(13 lines) is a redirect to the wiki; new contributors will look here first and bounce.
Aggregated from §8 and the repo-level survey.
- Twin migrations stall the frontend story. JS→TS and class→hooks are both partial. Each new component requires a style decision; mixed files discourage refactors. Either commit to finishing one migration or freeze the other.
- Provider proliferation without abstraction. Each new download
client / indexer / notification produces a near-clone of the previous
one. A shared
IProxyClientfor RestSharp boilerplate would shave hundreds of duplicate lines. Parser.csregex fragility. Release-name patterns shift constantly;Parser.csis regex-only with no test fixture documenting which patterns matter most. Crowdsourced edge cases land in this file and rarely get refactored.- Dual-DB dialect drift. Every new query has to be valid SQLite AND
Postgres. Date/time handling in particular has dedicated migrations
(e.g., for Postgres
timestamptz). Without a regression suite that runs on both, dialect drift is easy to ship. - Selenium suite is years behind. Selenium 3 / Chrome 91 means the automation tests test a fictional browser. Either upgrade in lockstep with Chrome or retire and replace with Playwright.
- macOS-11 retirement will break CI when Azure removes the image. Plan the macos-12/macos-13 bump now.
- No SBOM / no dependency scanning. Several vendored forks
(
Servarr.FluentMigrator,System.Data.SQLite.Core.Servarr,TagLibSharp-Lidarr) plus a vendored Win32 interop DLL mean the supply- chain story is opaque. Adding--vulnerablescanning to CI is cheap. - CI matrix's split StyleCop gating can let style regressions land if a contributor only ever hits the Windows/Mac legs.
NzbDrone.*namespace retention is a paper cut every time a new contributor reads the code. Either complete the rename or document the "namespace = NzbDrone, identity = Readarr" rule somewhere obvious (this doc is now one such place).- Project is retired. Upstream development stopped on 2025-06-27 with
the "Retirement announcement" commit. The repo is archived on GitHub —
issues and PRs are closed. Metadata source has become unusable and the
community Open-Library transition stalled (per the retirement notice
in
README.md:1-20). Any future work happens in third-party forks (the notice points torreading-glassesas the most popular mirror). The legacy "currently in beta testing" disclaimer further down in the README never made it to a stable release before retirement.
| Want to… | Go to |
|---|---|
| Add an indexer | src/NzbDrone.Core/Indexers/ — extend HttpIndexerBase |
| Add a download client | src/NzbDrone.Core/Download/Clients/ — extend TorrentClientBase or UsenetClientBase |
| Add a notification | src/NzbDrone.Core/Notifications/ — extend NotificationBase |
| Add an import list | src/NzbDrone.Core/ImportLists/ — extend HttpImportListBase |
| Add a database column | src/NzbDrone.Core/Datastore/Migration/0XX_Whatever.cs (FluentMigrator), plus update the model |
| Add an API endpoint | src/Readarr.Api.V1/{Domain}/{Entity}Controller.cs + matching {Entity}Resource.cs |
| Add a background job | src/NzbDrone.Core/Jobs/ + define an ICommand and IExecute<TCommand> handler |
| Add a SignalR push | src/Readarr.Api.V1/{Domain}/{Entity}ControllerWithSignalR.cs pattern; publish an event |
| Add a UI page | frontend/src/{Feature}/ + route in frontend/src/App/AppRoutes.js |
| Add a Redux slice | frontend/src/Store/Actions/{feature}Actions.js using the Creators/ factories |
| Trace a request end-to-end | Bootstrap.cs → Startup.cs → Readarr.Http middleware → Readarr.Api.V1/{Entity}Controller → NzbDrone.Core/{Domain}/{Entity}Service → Datastore/{Entity}Repository |
| Find StyleCop rules | src/stylecop.json + src/Stylecop.ruleset |
| Change the build matrix | azure-pipelines.yml |
| Change the bundled RID list | src/Directory.Build.props:11 |
Per-directory README.md files inside each major project / folder repeat
the relevant slice of this map locally.