Files
bookorbit/server
Oliver 6b0f561c76 fix(metadata): split embedded subtitle out of long hardcover titles (#887)
* fix(metadata): split embedded subtitle out of long hardcover titles

Hardcover's subtitle field is legacy: it is fed by their data providers but
ignored and unmaintained, so librarians follow the convention of embedding
the subtitle in the title after a colon. Imports from search and from
editions without their own subtitle therefore stored the full
"Title: Subtitle" string as the title while the subtitle stayed empty.

Mirror Hardcover's own display rule, confirmed by their developers: split
the title on the first colon once it exceeds 60 characters. An API-provided
subtitle keeps precedence and leaves the title untouched, and the length
threshold protects short colon titles like "2001: A Space Odyssey" from
false splits.

Closes #867

* fix(metadata): trim hardcover titles that duplicate the api subtitle

Books like hardcover.app/books/quantitative-momentum carry the embedded
subtitle in the librarian-curated title and additionally expose the same
text through the legacy edition subtitle. The subtitle precedence rule
then skipped the split and the imported title kept the duplicated
subtitle. When the part after the first colon matches the API subtitle
(case-insensitive, trimmed) and the title is over the 60-character
threshold, trim the title and keep the API subtitle verbatim. Titles
whose subtitle genuinely differs stay untouched.

Ref #867

* refactor(metadata): tighten hardcover embedded-subtitle split helper

Narrow splitEmbeddedSubtitle to the non-optional title both call sites
already pass, which removes the unreachable `?? doc.title` fallbacks, and
rename the threshold constant so it no longer reads as an off-by-one:
titles up to 60 characters are left unsplit.

Treat a whitespace-only API subtitle as absent so it no longer blocks the
split, and cover two interactions that had none: the blank-subtitle case,
and a relevance query carrying the full combined title still ranking the
split candidate first.

---------

Co-authored-by: ohoeltke <hoeltke@oho-dt.de>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-05 16:33:05 -06:00
..

BookOrbit - Server

NestJS 11 backend running on Fastify.

Running

From the repo root: pnpm dev (starts both server and client).

Server only: pnpm start:dev (from this directory).

The server loads environment variables from .env in this directory. See .env.example for all available options.

API docs

OpenAPI docs are disabled by default. Enable them with SWAGGER_ENABLED=true:

SWAGGER_ENABLED=true pnpm start:dev

Then open:

Dev watch uses a Nest config without the Swagger metadata generator so the server does not restart immediately after startup. Full build output still uses the standard Nest config and generates src/metadata.ts for enriched docs.

Module structure

Each feature is a self-contained NestJS module under src/modules/:

src/modules/
├── auth/           Authentication (JWT, OIDC, password reset)
├── book/           Book CRUD, cover images, file serving
├── library/        Library management
├── scanner/        File system scanning, metadata extraction
├── collection/     User book collections
├── smart-scope/           Saved search/filter views
├── user/           User management
├── role/           Roles and permissions (RBAC)
├── metadata/       Book metadata providers
├── kobo/           Kobo device sync
├── annotation/     Highlights and annotations
├── bookmark/       Reading position bookmarks
├── reader-preferences/  Per-user reader settings
├── cbz/            CBZ archive handling
├── path/           File path utilities
├── seed/           Database seeding
└── app-settings/   Global application settings

Database

Schema files live in src/db/schema/, one per domain. Migrations are generated by Drizzle Kit.

pnpm db:generate add_some_column   # generate migration from schema diff
pnpm db:migrate                     # apply pending migrations
pnpm db:studio                      # open Drizzle Studio (DB browser)

Testing

pnpm test           # unit tests
pnpm test:watch     # unit tests in watch mode
pnpm e2e:run        # app-smoke end-to-end test from server package
pnpm test:cov       # unit tests with coverage

For suite-based e2e runs, run from the repository root:

pnpm run e2e:list
pnpm run e2e:all
pnpm run e2e:run -- scanner-scenarios

Quality

pnpm type-check      # TypeScript checks (no emit)
pnpm lint:check      # Non-mutating lint checks
pnpm lint:fix        # Auto-fix lint issues
pnpm format:check    # Prettier check
pnpm format          # Prettier write

Key conventions

  • Constructor injection with @Inject(DB) for the Drizzle instance.
  • Config via @nestjs/config named configs (appConfig, dbConfig, authConfig, storageConfig, mailerConfig). Never read process.env directly in services.
  • DTOs use class-validator decorators. Global ValidationPipe rejects unknown fields.
  • Throw NestJS HttpException subclasses (NotFoundException, ForbiddenException, etc.) - never raw Error.
  • All user-owned data is scoped by userId. Service methods check ownership.