# /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.
