197 lines
8.6 KiB
Markdown
197 lines
8.6 KiB
Markdown
# Movies Diary
|
||
|
||
A self-hosted, server-side rendered movie logging system with a full REST API. Built in Rust — no JavaScript in the HTML interface, just HTML forms and an RSS feed. Designed to run as a lightweight widget embedded on a personal site or as a backend for third-party clients.
|
||
|
||
## Features
|
||
|
||
- Log movies with a TMDB/OMDb ID or manual title/year/director, with a 0–5 rating
|
||
- Immutable append-only viewing ledger (tracks re-watches)
|
||
- Background poster fetching and storage (local filesystem or S3-compatible)
|
||
- Movie enrichment via TMDb — full cast, crew, genres, keywords, runtime, budget/revenue, ratings; fetched automatically on movie discovery and refreshed every 30 days; exposed via `GET /api/v1/movies/{id}/profile`
|
||
- Full-text search across movies and people via `GET /api/v1/search` — free-text query plus structured filters (genre, year, person, department, language); backed by SQLite FTS5 or PostgreSQL tsvector + GIN indexes
|
||
- People as first-class entities — browse by person via `GET /api/v1/people/{id}` and full credit history via `GET /api/v1/people/{id}/credits`; index populated automatically during TMDb enrichment
|
||
- RSS/Atom feed for public subscription (global and per-user)
|
||
- JWT authentication via cookie (HTML) or Bearer token (REST API)
|
||
- ActivityPub federation — follow/unfollow remote users, accept/reject/remove followers, federated reviews broadcast as `Note` objects with `#MoviesDiary` + `#MovieTitle` hashtags, paginated outbox, boost/Announce tracking, NodeInfo discovery endpoint, shared inbox delivery, actor profile sync (bio, avatar, discoverable)
|
||
- Federation moderation — instance-level domain blocking (admin-managed), per-user actor blocking with `Block` activity, delivery filter excludes blocked actors and blocked-domain inboxes
|
||
- CSV and JSON diary export
|
||
- File importer: upload CSV, TSV, JSON, or XLSX from any source (Letterboxd, IMDb, etc.), map columns to domain fields via a step-by-step wizard or REST API, save mapping profiles for repeat imports
|
||
- REST API v1 (`/api/v1/`) with full feature parity with the HTML interface
|
||
- OpenAPI documentation at `/docs` (Swagger UI) and `/scalar` (Scalar)
|
||
- CSRF protection on all HTML form routes (double-submit cookie, defense-in-depth on top of `SameSite=Strict`)
|
||
- Per-IP rate limiting via token bucket (production-grade, backed by `axum-governor`)
|
||
- Terminal UI client (`crates/tui`) for logging reviews, bulk CSV import, and diary browsing
|
||
|
||
## Architecture
|
||
|
||
Hexagonal (Ports & Adapters) with Domain-Driven Design:
|
||
|
||
```
|
||
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas); used by presentation and tui
|
||
domain — pure types and trait definitions, no external deps
|
||
application — use cases / business logic orchestration
|
||
presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving, composition root for the HTTP process
|
||
worker — standalone worker binary (event consumer, poster sync, federation)
|
||
adapters/
|
||
auth — JWT issuance and validation (Argon2 passwords)
|
||
sqlite — SQLite repository + connection factory
|
||
postgres — PostgreSQL repository + connection factory
|
||
metadata — TMDB / OMDb HTTP client
|
||
poster-fetcher — downloads poster images
|
||
image-storage — stores images (posters + user avatars) on local filesystem or S3-compatible storage
|
||
poster-sync — event handler: triggers poster fetch+store on MovieDiscovered
|
||
image-converter — optional background worker: converts stored images to AVIF or WebP; backfills existing images via a 24h periodic job
|
||
tmdb-enrichment — event handler: fetches full movie profile (cast, crew, genres, keywords, box office) from TMDb on MovieEnrichmentRequested; resolves IMDb IDs automatically
|
||
template-askama — Askama HTML rendering
|
||
rss — RSS/Atom feed generation
|
||
export — CSV and JSON diary serialization
|
||
importer — CSV/TSV/JSON/XLSX parser and column mapper for bulk import
|
||
event-payload — shared event serialization DTOs (used by all event bus adapters)
|
||
sqlite-event-queue — durable polling event queue backed by SQLite
|
||
postgres-event-queue — durable polling event queue backed by PostgreSQL
|
||
nats — NATS Core / JetStream event publisher and consumer
|
||
event-publisher — in-memory event channel (used in tests)
|
||
activitypub — ActivityPub federation wiring (follow, inbox/outbox, actor)
|
||
activitypub-base — core ActivityPub protocol types and service
|
||
sqlite-search — SQLite FTS5 implementation of SearchPort + SearchCommand
|
||
postgres-search — PostgreSQL tsvector + GIN implementation of SearchPort + SearchCommand
|
||
sqlite-federation — SQLite-backed federation repository
|
||
postgres-federation — PostgreSQL-backed federation repository
|
||
tui — terminal UI client (ratatui); shares api-types with presentation for typed API access
|
||
```
|
||
|
||
## Prerequisites
|
||
|
||
- Rust (stable, 2024 edition)
|
||
- SQLite
|
||
- Poster storage: local filesystem (zero deps) or an S3-compatible object store (e.g. MinIO)
|
||
- An [OMDb API key](https://www.omdbapi.com/apikey.aspx)
|
||
|
||
## Environment Variables
|
||
|
||
A `.env.example` file is provided at the repo root — copy it to `.env` and fill in your values.
|
||
|
||
```env
|
||
# Database
|
||
DATABASE_URL=sqlite://movies.db
|
||
|
||
# Authentication
|
||
JWT_SECRET=change-me
|
||
|
||
# OMDb metadata
|
||
OMDB_API_KEY=your-key
|
||
|
||
# TMDb metadata + enrichment (optional — enables full cast/crew/genre data)
|
||
# TMDB_API_KEY=your-key
|
||
|
||
# Public base URL (used for ActivityPub actor URLs and canonical links)
|
||
BASE_URL=https://yourdomain.example.com
|
||
|
||
# Image storage — pick one backend:
|
||
|
||
# Option A: local filesystem (zero deps)
|
||
IMAGE_STORAGE_BACKEND=local
|
||
IMAGE_STORAGE_PATH=./images
|
||
|
||
# Option B: S3-compatible (MinIO, AWS S3, etc.)
|
||
# IMAGE_STORAGE_BACKEND=s3
|
||
# MINIO_ENDPOINT=http://localhost:9000
|
||
# MINIO_BUCKET=posters
|
||
# MINIO_REGION=minio
|
||
# MINIO_ACCESS_KEY_ID=minioadmin
|
||
# MINIO_SECRET_ACCESS_KEY=minioadmin
|
||
|
||
# Image conversion (optional — converts stored images to AVIF or WebP to save space)
|
||
# IMAGE_CONVERSION_ENABLED=false
|
||
# IMAGE_CONVERSION_FORMAT=avif # avif or webp
|
||
|
||
# Optional
|
||
HOST=0.0.0.0
|
||
PORT=3000
|
||
RATE_LIMIT=60 # requests per minute per IP (default: 60)
|
||
ALLOW_REGISTRATION=true # set to false to disable new sign-ups
|
||
SECURE_COOKIES=true # set when serving over HTTPS
|
||
RUST_LOG=presentation=info,tower_http=info,worker=info,application=info
|
||
|
||
# Event bus — "db" (default, uses same database) or "nats"
|
||
EVENT_BUS_BACKEND=db
|
||
# NATS_URL=nats://localhost:4222 # required when EVENT_BUS_BACKEND=nats
|
||
```
|
||
|
||
The `worker` binary must run alongside `presentation` to process events:
|
||
|
||
```bash
|
||
cargo run -p worker
|
||
```
|
||
|
||
## Run
|
||
|
||
```bash
|
||
cargo run -p presentation # HTTP server (0.0.0.0:3000)
|
||
cargo run -p worker # event worker (poster sync, in a separate terminal)
|
||
```
|
||
|
||
The worker polls the event queue and must run alongside the presentation to process background tasks like poster fetching. Both processes share the same database.
|
||
|
||
## API
|
||
|
||
All REST endpoints are under `/api/v1/`. Authentication uses `Authorization: Bearer <token>` obtained from `POST /api/v1/auth/login`.
|
||
|
||
Interactive API documentation is available at runtime:
|
||
|
||
- **Swagger UI** — `http://localhost:3000/docs`
|
||
- **Scalar** — `http://localhost:3000/scalar`
|
||
|
||
## Terminal UI
|
||
|
||
```bash
|
||
cargo run -p tui
|
||
```
|
||
|
||
Supports review logging, bulk CSV import (column order matches the export format), and diary browsing with review history.
|
||
|
||
## Test
|
||
|
||
```bash
|
||
cargo test
|
||
```
|
||
|
||
## Docker
|
||
|
||
The image contains both `presentation` (HTTP server) and `worker` (event processor). Run them as separate containers sharing the same data volume:
|
||
|
||
```bash
|
||
# Build (SQLite + federation + NATS support)
|
||
docker build -t movies-diary \
|
||
--build-arg FEATURES=sqlite,sqlite-federation,nats .
|
||
|
||
# HTTP server
|
||
docker run -p 3000:3000 \
|
||
-e DATABASE_URL=sqlite:///data/movies.db \
|
||
-e JWT_SECRET=change-me \
|
||
-e OMDB_API_KEY=your-key \
|
||
-e BASE_URL=https://yourdomain.example.com \
|
||
-e EVENT_BUS_BACKEND=nats \
|
||
-e NATS_URL=nats://nats:4222 \
|
||
-v $(pwd)/data:/data \
|
||
movies-diary
|
||
|
||
# Event worker (separate container, same image)
|
||
docker run \
|
||
-e DATABASE_URL=sqlite:///data/movies.db \
|
||
-e JWT_SECRET=change-me \
|
||
-e OMDB_API_KEY=your-key \
|
||
-e BASE_URL=https://yourdomain.example.com \
|
||
-e EVENT_BUS_BACKEND=nats \
|
||
-e NATS_URL=nats://nats:4222 \
|
||
-v $(pwd)/data:/data \
|
||
--entrypoint ./worker \
|
||
movies-diary
|
||
```
|
||
|
||
To build for PostgreSQL: `--build-arg FEATURES=postgres,postgres-federation,nats`
|
||
|
||
## License
|
||
|
||
MIT License. See [LICENSE](LICENSE).
|