# /seen/ /seen/ is a modern self-hosted platform for managing personal movies, TV shows, and games. It combines the functionality of a media tracker, streaming dashboard, download manager, and recommendation engine into a single application. The system allows users to discover content, track watched media, manage downloads, watch their personal media library, and manage a game backlog from a single modern dashboard-style interface. /seen/ is designed to feel like a **personal streaming control center** that runs entirely on your own infrastructure. --- # Core Philosophy The platform should be: - self-hosted - fast - modular - visually premium - simple to operate - scalable This project should **not resemble a generic admin dashboard**. Instead it should feel like a combination of: - Plex - Letterboxd - Netflix-style UI - personal media server - game backlog manager --- # Core Features ## Personal Media Tracking Users can: - mark movies as watched - mark movies or shows as watch later - track TV episode progress - track game completion states - organize active, paused, completed, and wishlist games - maintain personal collections - assign personal ratings - mark favorites - view watch history --- ## Continue Watching Playback progress is tracked per user. Features include: - resume playback - per-episode progress tracking - automatic watched detection - dashboard continue watching section Fast reads for progress are cached using DragonflyDB. --- ## Discover Content Content discovery is powered by TMDB for movies and TV, and IGDB for games. Sections include: - trending - popular - top rated - upcoming - now playing - airing today - recently released games - most anticipated games - genre browsing Users can also perform global search. Game metadata should be fetched through the backend using IGDB and Twitch OAuth credentials, never directly from the browser. --- ## Recommendations Recommendations are based on: - user watch history - favorite genres - TMDB similarity - IGDB genre, platform, and franchise similarity - internal recommendation scoring Recommendation results may be cached for performance. --- ## Download Manager Users can add downloads through: - magnet links - torrent files - direct download links - HTTP URLs The download manager handles: - job queue - progress - download speed - estimated completion - status reporting Download states include: - queued - downloading - stalled - completed - failed Downloaded files are stored on mounted storage volumes. --- ## Media Library Downloaded or imported files are scanned and linked to TMDB metadata. Users can: - browse their media library - stream media inside the platform - access files via mounted storage - view metadata and artwork A background **media scanner service** automatically matches files to movies or shows. --- ## Built-in Player The application includes a browser-based player. Capabilities: - playback resume - subtitle support - episode autoplay - theater mode Playback progress updates during viewing. --- ## Release Calendar Calendar showing upcoming releases. Includes: - movie premieres - new TV episodes - next season releases - game launches - expansion and DLC release windows Users can filter by: - watchlist - movie vs show - game platform - genre --- ## Dashboard The dashboard is the main hub. Widgets include: - continue watching - watch later - game backlog - recently played - active downloads - recommendations - upcoming releases - trending media - recently watched --- # Technology Stack ## Backend - Golang - Gin web framework - PostgreSQL - sqlc - goose migrations - zap structured logging - DragonflyDB caching Backend responsibilities: - API - authentication - metadata management - downloads orchestration - background jobs - TMDB integration - IGDB integration --- ## Frontend - SolidJS - TypeScript - Vite - TailwindCSS - shadcn-style component system - Ark UI primitives The frontend should behave like a **modern SaaS dashboard**. --- ## Infrastructure - Docker - Docker Compose - Nginx reverse proxy The system must be easy to deploy on a home server or VPS. --- # UI Design Design inspiration: https://haptics.lochie.me/ The UI should feel: - minimal - spacious - elegant - modern - smooth Subtle motion and polished transitions should be used. --- # Color Palette Primary accent #00b7ff Secondary accent #5800a9 Base background #111111 The color scheme should be used subtly to maintain a premium appearance. --- # Themes The platform must support: - dark mode - light mode - system preference detection - persistent theme storage Dark mode should be the primary visual style. --- # Application Layout Main application routes: /app/dashboard /app/discover /app/movies /app/shows /app/watch-later /app/watched /app/downloads /app/calendar /app/recommendations /app/library /app/collections /app/settings /app/admin The layout consists of: - sidebar navigation - top bar with search and profile - dashboard content area --- # Backend Architecture Directory layout example: cmd/ internal/ api/ services/ repositories/ domain/ middleware/ workers/ integrations/ downloader/ scanner/ pkg/ migrations/ sql/ The architecture separates: - transport layer - business logic - data access - integrations --- # Background Workers Background services handle tasks such as: - TMDB metadata synchronization - recommendation generation - download monitoring - media scanning - scheduled cleanup --- # Downloader Service Downloader responsibilities include: - torrent downloads - HTTP downloads - queue management - status reporting - file path tracking The downloader should be modular so engines can be replaced later. --- # Media Scanner The scanner links downloaded files to metadata. Process: 1. scan library folders 2. parse file names 3. query TMDB 4. match metadata 5. create library entries --- # Database PostgreSQL acts as the primary data store. Main tables include: users sessions movies tv_shows seasons episodes genres user_watch_status user_watchlist user_progress user_ratings collections collection_items downloads download_files library_items Indexes and foreign keys must be properly defined. --- # Caching Strategy DragonflyDB is used for: - session storage - API caching - TMDB response cache - continue watching cache - search suggestions - recommendation snapshots PostgreSQL remains the source of truth. --- # API All endpoints follow versioned REST design. Example routes: /api/v1/auth /api/v1/movies /api/v1/shows /api/v1/search /api/v1/watchlist /api/v1/progress /api/v1/downloads /api/v1/library /api/v1/recommendations Endpoints must support pagination and filtering. --- # Authentication Authentication system features: - email and password login - secure password hashing - refresh token or session strategy - role-based access Roles include: user admin --- # Security Security measures include: - input validation - rate limiting - secure cookie handling - CSRF protection - environment-based configuration - safe file handling --- # Deployment The application runs in Docker containers. Services include: seen-backend seen-frontend postgres dragonfly nginx downloader The system should run with a single docker compose setup. --- # Long-Term Vision /seen/ aims to become a complete self-hosted media platform combining the strengths of: - Plex - Letterboxd - Netflix UI - personal media tracking All while remaining open, customizable, and fully controlled by the user. # UI Component Reference - https://pro.alignui.com/blocks Use this as visual reference for selected layout and component patterns. The final result should stay clean, minimal, and modern SaaS style while keeping the /seen/ brand direction. --- # Implementation Snapshot (March 10, 2026) This section reflects the repository state on **March 10, 2026**. ## Frontend status Implemented: - app shell (sidebar + top bar) - route guards and auth session bootstrap - dashboard page with live widgets contract - discover page with search, filters, pagination behavior - watch later page with authenticated add/remove flow - continue watching widget actions wired to persisted progress updates - dark/light/system theme persistence - typed service layer with mock/live API switch Scaffolded but still placeholder pages: - /app/movies - /app/shows - /app/watched - /app/downloads - /app/calendar - /app/recommendations - /app/library - /app/collections - /app/settings - /app/admin ## Backend status Implemented: - Gin server with request id, access logging, and recovery middleware - health endpoints and readiness checks - auth endpoints: register, login, refresh, me - JWT access token + persisted refresh sessions - live catalog endpoints: dashboard, continue watching, discover, search - authenticated watch later endpoints: list, add, remove - authenticated progress update endpoint with continue watching refresh - persistent catalog schema for media, genres, and discover sections - seeded catalog dataset for discover/search/dashboard rails - PostgreSQL catalog repository wired into catalog service - user_watch_later persistence with per-user ordering - user_progress persistence with ordered continue watching reads - worker manager with downloader and scanner worker scaffolding Not implemented yet: - write endpoints for watched history, ratings, favorites, and downloads - real TMDB synchronization jobs - production downloader engine integration - scanner-to-library persistence flow ## Database status Current migration provides: - users - sessions - media_items - genres - media_genres - discover_sections - discover_section_items - user_watch_later - user_progress Remaining domain tables are planned below in the Data Model Expansion Plan. --- # Current API Surface (v1) ## Live endpoints - GET /api/v1/health/live - GET /api/v1/health/ready - POST /api/v1/auth/register - POST /api/v1/auth/login - POST /api/v1/auth/refresh - GET /api/v1/auth/me - GET /api/v1/dashboard - GET /api/v1/progress/continue-watching - GET /api/v1/discover - GET /api/v1/search - GET /api/v1/watch-later - POST /api/v1/watch-later - DELETE /api/v1/watch-later/:mediaId - POST /api/v1/progress ## Placeholder endpoints - GET /api/v1/movies - GET /api/v1/shows - GET /api/v1/watched - GET /api/v1/watchlist - GET /api/v1/progress - GET /api/v1/downloads - GET /api/v1/calendar - GET /api/v1/library - GET /api/v1/collections - GET /api/v1/settings - GET /api/v1/admin - GET /api/v1/recommendations --- # Phase Roadmap ## Phase 2A - Persistent catalog and metadata foundation Goals: - move catalog read models from in-memory seed data to PostgreSQL - keep the current frontend API contracts stable Deliverables: - tables for media, genres, and linking tables - repository + sqlc queries for catalog reads - TMDB sync job for trending/popular/upcoming datasets - cache keys and TTL policy for discovery and dashboard aggregates Exit criteria: - /dashboard, /discover, and /search responses come from persisted data - API response shape remains backward compatible with current frontend types ## Phase 2B - User tracking and watch flows Goals: - enable user-specific state changes from UI interactions Deliverables: - watch later CRUD endpoints - watched history writes - per-episode progress updates - rating and favorites persistence Exit criteria: - users can add/remove watch later items - users can mark watched and resume playback positions - dashboard widgets reflect real per-user data ## Phase 2C - Download manager (real jobs) Goals: - replace placeholder download data with executable jobs Deliverables: - create/list/cancel download endpoints - queue persistence + job state transitions - worker integration with selected download engine - progress events polling contract for UI Exit criteria: - a submitted download moves through queued/downloading/completed or failed - dashboard active downloads widget is fully live ## Phase 3A - Library and scanner Goals: - connect downloaded/imported files into a browsable library Deliverables: - scanner pipeline: file discovery, parsing, metadata matching, persistence - library browse endpoints with filters - artwork and metadata hydration strategy Exit criteria: - new media files appear in /library with matched metadata - unmatched files are visible in a review queue ## Phase 3B - Recommendations and calendar Goals: - move recommendation and calendar from static placeholders to generated data Deliverables: - recommendation scoring job - release calendar endpoint with watchlist filters - cache snapshot pipeline for fast dashboard reads Exit criteria: - recommendation list and calendar are generated per user profile --- # Data Model Expansion Plan Already present (migrations 000001 to 000004): - users - sessions - media_items - genres - media_genres - discover_sections - discover_section_items - user_watch_later - user_progress Planned next: - media_external_ids - show_seasons - show_episodes - user_watched - user_ratings - user_favorites - collections - collection_items - download_jobs - download_files - download_events - library_items - library_streams Index priorities: - (user_id, updated_at) on user activity tables - (media_type, release_date) on media browsing tables - (status, created_at) on download_jobs - GIN/trigram search index for title/overview search --- # Quality Gates Before merging major slices: 1. backend unit tests pass (`go test ./...`) 2. frontend unit tests pass (`npm run test:unit`) 3. frontend smoke e2e passes (`npm run test:e2e`) 4. docker compose stack boots and health endpoints are green 5. no placeholder endpoint is silently reused for production behavior --- # Immediate Next Build Order 1. Start TMDB sync worker to refresh discover sections in PostgreSQL. 2. Add `download_jobs` schema + worker state machine for Phase 2C. 3. Add cached dashboard aggregate reads in DragonflyDB. 4. Replace static dashboard recommendations with persisted recommendation snapshots. 5. Add watched history write endpoints and UI hooks. --- # Phase 2C Delivery Blueprint (Downloads) ## Scope Turn `/api/v1/downloads` from placeholder into a real per-user job system with queue state transitions, progress updates, cancellation, and dashboard integration. ## Schema draft `download_jobs` - id (uuid, primary key) - user_id (uuid, not null, fk users.id) - source_type (text, check in `magnet|torrent|direct|http`) - source (text, not null) - title (text, not null default '') - status (text, check in `queued|preparing|downloading|stalled|retrying|completed|failed|cancelled`) - queue_position (integer, nullable) - progress_percent (integer, not null default 0, check 0-100) - bytes_total (bigint, not null default 0) - bytes_downloaded (bigint, not null default 0) - download_speed_mbps (double precision, not null default 0) - eta_seconds (integer, nullable) - engine_job_id (text, nullable, unique) - error_message (text, not null default '') - retry_count (smallint, not null default 0) - created_at (timestamptz, not null default now()) - updated_at (timestamptz, not null default now()) - started_at (timestamptz, nullable) - completed_at (timestamptz, nullable) - cancelled_at (timestamptz, nullable) `download_events` - id (bigserial, primary key) - job_id (uuid, not null, fk download_jobs.id on delete cascade) - status (text, not null) - message (text, not null default '') - progress_percent (integer, not null default 0) - payload (jsonb, not null default '{}'::jsonb) - created_at (timestamptz, not null default now()) Indexes: - `(user_id, created_at desc)` on `download_jobs` - `(user_id, status, updated_at desc)` on `download_jobs` - `(status, created_at asc)` partial where status in (`queued`, `retrying`) - `(job_id, created_at desc)` on `download_events` ## State machine Transitions: - queued -> preparing - preparing -> downloading - downloading -> stalled - stalled -> retrying - retrying -> preparing - downloading -> completed - downloading -> failed - queued|preparing|downloading|stalled|retrying -> cancelled Rules: - progress is monotonic for active states - completed implies `progress_percent = 100` - cancelled jobs keep historical events and are excluded from active dashboard count - retries capped (`retry_count <= 3`) before terminal `failed` ## API contract draft `POST /api/v1/downloads` - request body: - `sourceType`: `magnet|torrent|direct|http` - `source`: non-empty URL/magnet - `title`: optional UI label - response: - `201 Created` - `{ "id": "...", "status": "queued", ... }` `GET /api/v1/downloads` - query params: - `status` optional - `limit` default 20 - `cursor` optional for pagination - response: - ordered by newest activity - includes progress and timing fields `DELETE /api/v1/downloads/:id` - semantics: - soft cancel if job is active - no-op if already terminal - response: - `200 OK` with updated job payload `GET /api/v1/downloads/:id/events` - query params: - `after` optional RFC3339 timestamp - `limit` default 100 - response: - ordered events for timeline and debugging ## Worker split `downloader-dispatcher`: - pulls `queued/retrying` jobs - reserves available concurrency slots - sends Add call to selected engine `downloader-monitor`: - polls engine status - writes progress updates + events - finalizes terminal states Runtime defaults: - `DOWNLOAD_MAX_ACTIVE=3` - `DOWNLOAD_POLL_INTERVAL=3s` - `DOWNLOAD_RETRY_BACKOFF=15s` ## Exit criteria for Phase 2C - dashboard `activeDownloads` comes from persisted jobs - creating a job from API moves it through real states - cancellation works for both queued and active jobs - smoke e2e covers submit -> progress -> complete/cancel path --- # Phase 3A Delivery Blueprint (Library + Scanner) ## Scope Build a durable library index for local media files and expose browse + matching workflows. ## Schema draft `library_items` - id (uuid, primary key) - media_id (bigint, nullable, fk media_items.id) - source_job_id (uuid, nullable, fk download_jobs.id) - absolute_path (text, not null, unique) - file_name (text, not null) - file_size_bytes (bigint, not null default 0) - container_format (text, not null default '') - video_codec (text, not null default '') - audio_codec (text, not null default '') - duration_seconds (integer, not null default 0) - resolution_label (text, not null default '') - match_state (text, check in `matched|unmatched|ignored`) - match_confidence (double precision, not null default 0) - discovered_at (timestamptz, not null default now()) - scanned_at (timestamptz, nullable) - created_at (timestamptz, not null default now()) - updated_at (timestamptz, not null default now()) `library_streams` - id (uuid, primary key) - library_item_id (uuid, not null, fk library_items.id on delete cascade) - stream_type (text, check in `video|audio|subtitle`) - codec (text, not null default '') - language (text, not null default '') - channels (smallint, not null default 0) - bitrate_kbps (integer, not null default 0) - is_default (boolean, not null default false) Indexes: - `(match_state, updated_at desc)` on `library_items` - `(media_id, updated_at desc)` on `library_items` - trigram index on `file_name` for unmatched review search ## Scanner workflow 1. discover files under configured library roots 2. normalize path and deduplicate by `absolute_path` 3. parse title/season/episode hints from filename 4. call TMDB search/match pipeline 5. upsert `library_items` + `library_streams` 6. mark low-confidence matches as `unmatched` ## API contract draft `GET /api/v1/library` - filters: - `matchState` - `mediaType` - `query` - `page`, `pageSize` - returns enriched cards for UI browse grid `POST /api/v1/library/rescan` - optional body: - `paths`: array of roots - returns: - scan job id + accepted count `GET /api/v1/library/unmatched` - paginated unmatched queue with confidence metadata `POST /api/v1/library/:id/match` - request body: - `mediaId` - `force` optional bool - manually links file to a media entry ## Exit criteria for Phase 3A - new files are indexed within one scan cycle - unmatched queue is visible and manually resolvable - `/api/v1/library` is no longer placeholder - scanner worker persists state changes, not heartbeats only --- # Cache Key Contract (DragonflyDB) Use explicit versioned keys so invalidation is predictable: - `seen:v1:discover:{query_hash}` (TTL: 10m) - `seen:v1:dashboard:{user_id}` (TTL: 2m) - `seen:v1:continue-watching:{user_id}` (TTL: 2m) - `seen:v1:recommendations:{user_id}` (TTL: 30m) - `seen:v1:calendar:{user_id}:{month}` (TTL: 6h) Invalidation triggers: - watch-later change -> invalidate `dashboard` and `recommendations` - progress update -> invalidate `continue-watching` and `dashboard` - TMDB sync refresh -> invalidate discover keys - recommendation snapshot refresh -> invalidate recommendation key --- # File-Level Build Plan Backend additions: - `backend/migrations/000005_init_downloads.up.sql` - `backend/migrations/000005_init_downloads.down.sql` - `backend/migrations/000006_init_library.up.sql` - `backend/migrations/000006_init_library.down.sql` - `backend/sql/queries/downloads.sql` - `backend/sql/queries/library.sql` - `backend/internal/repositories/postgres/download_repository.go` - `backend/internal/repositories/postgres/library_repository.go` - `backend/internal/services/download/service.go` - `backend/internal/services/library/service.go` - `backend/internal/api/handlers/download_handler.go` - `backend/internal/api/handlers/library_handler.go` Frontend additions: - `frontend/src/services/download-service.ts` - `frontend/src/services/library-service.ts` - `frontend/src/pages/downloads-page.tsx` - `frontend/src/pages/library-page.tsx` - `frontend/src/pages/calendar-page.tsx` - `frontend/src/pages/recommendations-page.tsx` Tests to add: - backend unit tests for download state transitions - backend repository tests for scanner/library filtering - frontend unit tests for downloads and library stores/services - e2e flow covering download create/cancel and library browse --- # Updated Build Priority (Next 2 Sprints) Sprint A: 1. Implement download schema + SQL queries + repository. 2. Implement download service state machine + worker loops. 3. Ship `/api/v1/downloads` endpoints and replace dashboard download mock. 4. Add tests and smoke e2e for download lifecycle. Sprint B: 1. Implement library schema + scanner persistence pipeline. 2. Ship `/api/v1/library` list/rescan/unmatched/match endpoints. 3. Replace placeholder library page with real browse + unmatched queue. 4. Add TMDB sync job and cache invalidation wiring for discover/dashboard.