22 KiB
/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:
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:
- scan library folders
- parse file names
- query TMDB
- match metadata
- 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
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:
- backend unit tests pass (
go test ./...) - frontend unit tests pass (
npm run test:unit) - frontend smoke e2e passes (
npm run test:e2e) - docker compose stack boots and health endpoints are green
- no placeholder endpoint is silently reused for production behavior
Immediate Next Build Order
- Start TMDB sync worker to refresh discover sections in PostgreSQL.
- Add
download_jobsschema + worker state machine for Phase 2C. - Add cached dashboard aggregate reads in DragonflyDB.
- Replace static dashboard recommendations with persisted recommendation snapshots.
- 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)ondownload_jobs(user_id, status, updated_at desc)ondownload_jobs(status, created_at asc)partial where status in (queued,retrying)(job_id, created_at desc)ondownload_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 terminalfailed
API contract draft
POST /api/v1/downloads
- request body:
sourceType:magnet|torrent|direct|httpsource: non-empty URL/magnettitle: optional UI label
- response:
201 Created{ "id": "...", "status": "queued", ... }
GET /api/v1/downloads
- query params:
statusoptionallimitdefault 20cursoroptional 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 OKwith updated job payload
GET /api/v1/downloads/:id/events
- query params:
afteroptional RFC3339 timestamplimitdefault 100
- response:
- ordered events for timeline and debugging
Worker split
downloader-dispatcher:
- pulls
queued/retryingjobs - 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=3DOWNLOAD_POLL_INTERVAL=3sDOWNLOAD_RETRY_BACKOFF=15s
Exit criteria for Phase 2C
- dashboard
activeDownloadscomes 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)onlibrary_items(media_id, updated_at desc)onlibrary_items- trigram index on
file_namefor unmatched review search
Scanner workflow
- discover files under configured library roots
- normalize path and deduplicate by
absolute_path - parse title/season/episode hints from filename
- call TMDB search/match pipeline
- upsert
library_items+library_streams - mark low-confidence matches as
unmatched
API contract draft
GET /api/v1/library
- filters:
matchStatemediaTypequerypage,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:
mediaIdforceoptional 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/libraryis 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
dashboardandrecommendations - progress update -> invalidate
continue-watchinganddashboard - TMDB sync refresh -> invalidate discover keys
- recommendation snapshot refresh -> invalidate recommendation key
File-Level Build Plan
Backend additions:
backend/migrations/000005_init_downloads.up.sqlbackend/migrations/000005_init_downloads.down.sqlbackend/migrations/000006_init_library.up.sqlbackend/migrations/000006_init_library.down.sqlbackend/sql/queries/downloads.sqlbackend/sql/queries/library.sqlbackend/internal/repositories/postgres/download_repository.gobackend/internal/repositories/postgres/library_repository.gobackend/internal/services/download/service.gobackend/internal/services/library/service.gobackend/internal/api/handlers/download_handler.gobackend/internal/api/handlers/library_handler.go
Frontend additions:
frontend/src/services/download-service.tsfrontend/src/services/library-service.tsfrontend/src/pages/downloads-page.tsxfrontend/src/pages/library-page.tsxfrontend/src/pages/calendar-page.tsxfrontend/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:
- Implement download schema + SQL queries + repository.
- Implement download service state machine + worker loops.
- Ship
/api/v1/downloadsendpoints and replace dashboard download mock. - Add tests and smoke e2e for download lifecycle.
Sprint B:
- Implement library schema + scanner persistence pipeline.
- Ship
/api/v1/librarylist/rescan/unmatched/match endpoints. - Replace placeholder library page with real browse + unmatched queue.
- Add TMDB sync job and cache invalidation wiring for discover/dashboard.