Files
2026-06-03 10:40:00 +03:00

77 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
`mcma-backend` — a self-hosted, **offline-first** music service (FOSS, homelab scale: unitstens of users). Downloads music from external sources (yt-dlp etc.), stores files + metadata, streams to clients, serves a native REST API plus a Subsonic-compatible API. The server is a **sync layer**, not the sole source of truth — clients are expected to work offline, which shapes the data model (timestamps, event-logs) even before sync is built.
Status: **Phase 1 (MVP)**. Step 1 (skeleton) is done; the data model, auth, sources, enrichment, streaming, and Subsonic layer follow. There is no in-repo plan document — the build order and deferred work live in Claude's project memory.
## Commands
Uses **uv** (managed Python 3.14) for everything.
```bash
uv sync # install deps (runtime + dev group)
uv run uvicorn app.main:app --reload # run API (needs Postgres + Redis up)
uv run arq app.workers.arq_worker.WorkerSettings # run background worker
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy app # type-check (strict)
uv run pytest # all tests
uv run pytest tests/test_health.py::test_liveness_ok # single test
uv run alembic revision --autogenerate -m "msg" # new migration (after model changes)
uv run alembic upgrade head # apply migrations
docker build -t mcma-backend . # build this service's image (repo is infra-free)
```
Orchestration is **not** in this repo. The workspace compose (`../docker-compose.yml`,
shared with `mcma-webui`) wires the stack: `docker compose up -d` runs backing
services only (db + redis) for local dev; `docker compose --profile app up --build`
runs the full stack. The image takes every peer from env (`DATABASE_URL`, `REDIS_URL`,
`MEDIA_PATH`, …) and hardcodes nothing.
Tests run in-process against the ASGI app (httpx + asgi-lifespan) — no server, no network. They do **not** require a running DB/Redis: dependency-backed checks degrade rather than fail.
## Architecture — hexagonal (ports & adapters)
Dependencies point **inward**. The domain knows nothing about frameworks; outer layers are wired together only at the composition root.
- `app/domain/` — pure business core: entities, value objects, errors, and **ports** (Protocols). No framework imports (no FastAPI/SQLAlchemy/redis here, ever).
- `app/application/` — use cases / services. Depend on domain ports, never on concrete adapters.
- `app/infrastructure/` — driven adapters: ORM models + repositories (`db/`), Redis client (`cache/`), source backends, ML/HTTP clients. All vendor-specific code is confined here.
- `app/api/` — driving adapter (FastAPI): routers, schemas, `deps.py` (request-scoped wiring / composition root), middleware, error mapping.
- `app/core/` — cross-cutting: config, logging, security.
- `app/workers/` — arq background tasks (heavy I/O and CPU work).
**Composition roots** are `app/main.py` (`create_app`, lifespan) and `app/api/deps.py` (binds concrete adapters to ports per request). When adding a feature: define the port in `domain`, the service in `application`, the adapter in `infrastructure`, and wire it in `deps.py`.
### Cross-cutting conventions
- **Errors:** services raise framework-agnostic exceptions from `app/domain/errors.py`. `app/api/errors.py` is the *only* place that maps domain errors → HTTP status codes. Don't reference HTTP status from domain/application code.
- **Config:** `app/core/config.py``get_settings()` is a cached singleton over pydantic-settings. Everything comes from env (or `.env` in dev); nothing is hardcoded. `database_url` must use the `+asyncpg` driver (validated).
- **Logging:** `app/core/logging.py` — structlog, console in dev / JSON in prod. A `correlation_id` contextvar is bound per HTTP request (middleware) and should be bound per worker task; it auto-attaches to every log line.
- **DB sessions:** API uses `SessionDep` (request-scoped, commit-on-success/rollback-on-error). Workers/scripts use `session_scope()` from `app.infrastructure.db`.
- **Migrations:** Alembic is async and settings-driven — the URL is injected in `alembic/env.py`, never written to `alembic.ini`. Uncomment the models import in `env.py` once ORM models exist so autogenerate sees them.
- **Health:** `/health` is liveness; `/health/ready` checks db + redis (required) + ml (optional). Every dependency check is wrapped in `asyncio.timeout` — a readiness probe must never hang.
## Non-negotiable domain invariants
These exist for future sync + ML and are easy to violate by accident:
- **Likes are an append-only event-log, not a boolean.** Current state = the latest event per `(user, track)`. Never update a like row in place.
- **`track.id` is the stable `content_id`** — a uuid that never changes; it's the basis of client-side caching. Don't regenerate it.
- **Dedup on both** `(source, source_id)` **and** `acoustid_fingerprint`. Downloads/imports must be idempotent.
- **Graceful degradation is mandatory.** ML, music sources, MusicBrainz/AcoustID are all optional. One being down must degrade a feature, never crash the system. The ML service is *not* a required dependency — radio must still work (worse) without it.
- **Never overwrite `metadata_status = manual`** with automatic enrichment.
- **No heavy work in the request cycle.** ffmpeg, fingerprinting, downloads — all go to arq workers.
- **Subsonic compatibility is a strategic goal:** the Subsonic API is an adapter over services, not a reimplementation of logic.
## Python 3.14 note
The project targets Python 3.14, which makes annotations lazy by default (PEP 649). `from __future__ import annotations` is therefore intentionally **absent** — do not add it back. All pins (pyproject `requires-python`, ruff `target-version`, mypy `python_version`, Dockerfile, `.python-version`) are on 3.14.