# Unified Media Manager (UMM) > One platform to rule them all — replaces 12 Docker containers with one unified Go + React media management system. [![Build Status](https://github.com/TopherMayor/unified-media-manager/actions/workflows/test.yml/badge.svg)](https://github.com/TopherMayor/unified-media-manager) [![Go Version](https://img.shields.io/badge/Go-1.25-blue)](https://golang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ## Resume Summary **Unified Media Manager** — Designed and built a production-grade media management platform that consolidated 12 separate Docker containers (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, Jellyseerr, Bazarr, Recyclarr, and their anime variants) into a single Go + React service. - Reduced infrastructure from **12 containers to 1**, eliminating 11 redundant databases, 11 log schemas, and duplicated configuration - Built a multi-protocol indexer engine supporting Torznab/Newznab XML and Cardigann YAML definitions for compatibility with 50+ indexers - Implemented hardlink-based acquisition so downloads and library files share blocks on disk (zero extra storage) - Designed a PostgreSQL schema with 24 tables using partitioning, JSONB, and GIN indexes — replacing 82+ tables from the arr stack - Built a full-stack React UI with 12 pages: Dashboard, Library, Search, Discover, Queue, Activity, Blocklist, Calendar, Requests, Settings, MediaDetail, and Semantic Search - Implemented AI-powered semantic search using Qdrant vector database and Ollama embeddings for natural-language media discovery - Tech stack: Go 1.25, Echo v4, PostgreSQL 16, React 18 + TypeScript + TailwindCSS, Qdrant, Ollama, Docker ## What It Does UMM is a self-hosted media acquisition and management platform. You add movies, TV shows, music, books, or podcasts you want to watch/read/listen to, and UMM automatically: 1. **Searches** your configured indexers (via Cardigann or Torznab) for matching releases 2. **Evaluates** quality, seeders, and snatch speed to pick the best release 3. **Downloads** via qBittorrent or SABnzbd — routed through a VPN (Gluetun) for privacy 4. **Imports** completed downloads to your library using hardlinks (no extra disk space) 5. **Enriches** metadata from TMDB, TVDB, MusicBrainz, and OpenLibrary 6. **Tracks** everything in PostgreSQL with full-text search across your entire library 7. **Notifies** you via webhook/Telegram when media is ready 8. **Searches semantically** — "quirky indie documentaries about AI" returns relevant results ## Architecture ``` Internet │ ┌──────┴──────┐ │ Traefik │ TLS termination (existing infra) └──────┬──────┘ │ ┌─────────────┴─────────────┐ │ │ ┌──────┴──────┐ ┌───────┴───────┐ │ Nginx │ │ React UI │ │ :3000 │ │ (SPA) │ └──────┬──────┘ └───────▲────────┘ │ │ │ /api/* proxy_pass │ fetch() │ │ ┌─────────▼──────────────────────────┴───────┐ │ Go Backend :8084 │ │ │ │ ┌──────────┐ ┌────────────┐ ┌────────┐│ │ │ REST API │ │ Workers │ │Schedu- ││ │ │ (Echo v4)│ │ (queuemgr) │ │ler(cron││ │ └────┬─────┘ └─────┬──────┘ └────────┘│ │ │ │ │ └───────┼──────────────┼─────────────────────┘ │ │ ┌─────────┼──────────────┼────────────────────────┐ │ │ │ UM M Network │ │ ┌──────▼──────┐ ┌────▼─────┐ ┌─────────────┐│ │ │ PostgreSQL │ │ Qdrant │ │ Ollama ││ │ │ :5432 │ │ :6333 │ │ :11434 ││ │ │ (primary) │ │ (vectors)│ │ (AI/LLM) ││ │ └─────────────┘ └───────────┘ └─────────────┘│ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ Gluetun VPN Network │ │ │ │ SABnzbd :8080 │ qBittorrent :8085 │ │ │ └──────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘ Media files flow: Indexers → UMM (evaluate) → Download Client (via VPN) → Import (hardlink) → Library (NFS share) → Jellyfin/Navidrome/Calibre ``` ## Key Features | Feature | Description | |---------|-------------| | **Unified Search** | Search across all media types (movies, TV, music, books, podcasts) in a single query | | **Indexer Integration** | Cardigann YAML + Torznab/Newznab XML support — works with 50+ indexers | | **Smart Downloads** | Quality-first release selection with seeder/speed scoring | | **Hardlinks** | Completed downloads hard-linked to library — no extra storage | | **Quality Tracking** | Current vs. desired quality per item with auto-upgrade rules | | **Request System** | Built-in request/approval workflow — no separate Jellyseerr needed | | **Subtitle Search** | Automatic subtitle search across multiple sources | | **RSS Sync** | Monitor indexer RSS feeds for new releases automatically | | **Activity Feed** | Real-time event log: searches, downloads, imports, failures | | **Blocklist** | Block specific releases or indexers to prevent re-snatch | | **Semantic Search** | Natural language queries ("dark comedies from the 90s") via Qdrant + Ollama | | **Calendar View** | See upcoming releases by air date or your watchlist | | **Analytics Dashboard** | Disk usage, download stats, library breakdown by media type | ## Tech Stack | Layer | Technology | |-------|-----------| | Backend | Go 1.25 (Echo v4 HTTP framework) | | Frontend | React 18 + TypeScript + TailwindCSS + Vite | | Database | PostgreSQL 16 (partitioned tables, JSONB, GIN indexes) | | Vector DB | Qdrant (semantic search) | | AI | Ollama (nomic-embed-text embeddings, LLaVA vision) | | Download | qBittorrent, SABnzbd (via VPN tunnel) | | Indexers | Cardigann YAML + Torznab/Newznab XML | | Container | Docker + Docker Compose | | Reverse Proxy | Traefik (existing) | ## Getting Started ### Prerequisites - Docker + Docker Compose v3.8 - PostgreSQL 16 instance (or use the Docker default) - qBittorrent or SABnzbd (with VPN via Gluetun recommended) - TMDB API key (free at https://www.themoviedb.org/) ### Quick Start ```bash # Clone the repository git clone https://github.com/TopherMayor/unified-media-manager.git cd unified-media-manager # Copy and edit environment cp .env.example .env $EDITOR .env # Start the stack docker compose up -d # Run database migrations docker compose exec umm /umm migrate # Open the UI open http://umm.local.tophermayor.com ``` ### Configuration Environment variables (see `.env.example`): ```bash DATABASE_URL=postgres://user:pass@postgres-shared:5432/unified_media_manager?sslmode=disable QDRANT_URL=http://qdrant:6333 OLLAMA_URL=http://ollama:11434 TMDB_API_KEY=your_tmdb_api_key PORT=8084 ``` ### Docker Compose The stack uses Traefik for routing. Add this label to your `docker-compose.yml`: ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.umm.rule=Host(`umm.local.tophermayor.com`)" - "traefik.http.routers.umm.tls=true" ``` ## Project Structure ``` unified-media-manager/ ├── cmd/ │ └── migrate/ # Database migration CLI ├── internal/ │ ├── api/ # Echo HTTP handlers (12 resource files) │ │ ├── activity.go │ │ ├── blocklist.go │ │ ├── calendar.go │ │ ├── dashboard.go │ │ ├── discover.go │ │ ├── download_clients.go │ │ ├── health.go │ │ ├── import.go │ │ ├── indexers.go │ │ ├── media.go │ │ ├── metadata.go │ │ ├── notifications.go │ │ ├── quality.go │ │ ├── queue.go │ │ ├── requests.go │ │ ├── root_folder.go │ │ ├── router.go # Echo router setup │ │ ├── search.go │ │ ├── subtitle.go │ │ ├── tag.go │ │ └── workers.go │ ├── cardigann/ # Cardigann YAML indexer engine │ ├── config/ # Environment config loading │ ├── db/ # PostgreSQL pool + migrations │ │ └── migrations/ # 13 SQL migration files │ ├── download/ # qBittorrent + SABnzbd clients │ ├── migrate/ # arr migration tool (import from Sonarr/Radarr) │ ├── service/ # Business logic (30+ files) │ └── worker/ # Background workers (scheduler, queue, scanner...) ├── frontend/ │ └── src/ │ ├── api/ # TypeScript API client │ ├── components/ # Reusable UI components │ └── pages/ # 12 page components ├── scripts/ │ └── migrate-arrs.sh # Batch import from arr stack ├── docker-compose.yml ├── SPEC.md # Full technical specification └── AGENTS.md # AI agent coding conventions ``` ## API Endpoints (selected) | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/search?q=...&type=...` | Full-text search across library | | `GET` | `/api/search/semantic?q=...&k=5` | Semantic search via Qdrant + Ollama | | `GET` | `/api/media` | Paginated media list with filters | | `GET` | `/api/media/:id` | Media detail with releases | | `POST` | `/api/media/:id/download` | Trigger download for specific release | | `GET` | `/api/queue` | Active download queue | | `DELETE` | `/api/queue/:id` | Cancel queued download | | `GET` | `/api/discover/recommended` | AI-recommended media | | `POST` | `/api/indexers/search` | Search all configured indexers | | `GET` | `/api/activity` | Recent activity events | | `GET` | `/api/dashboard/stats` | Dashboard metrics | | `POST` | `/api/requests` | Create media request | | `GET` | `/api/health` | Health check | ## Contributing 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## License MIT License — see [LICENSE](LICENSE) for details.