Extract all I/O (subprocess, filesystem, YAML loading) from the domain layer via ports/adapters. domain/subtitles/ now has zero imports from infrastructure/. The remaining domain → infra leak (release knowledge loaded at import time) is documented in tech-debt for a dedicated branch.
Alfred Media Organizer 🎬
An AI-powered agent for managing your local media library with natural language. Search, download, and organize movies and TV shows effortlessly through a conversational interface.
✨ Features
- 🤖 Natural Language Interface — Talk to your media library in plain language
- 🔍 Smart Search — Find movies and TV shows via TMDB with rich metadata
- 📥 Torrent Integration — Search and download via qBittorrent
- 🧠 Contextual Memory — Remembers your preferences and conversation history
- 📁 Auto-Organization — Moves and renames media files, resolves destinations, handles subtitles
- 🎞️ Subtitle Pipeline — Identifies, matches, and places subtitle tracks automatically
- 🔄 Workflow Engine — YAML-defined multi-step workflows (e.g.
organize_media) - 🌐 OpenAI-Compatible API — Works with any OpenAI-compatible client (LibreChat, OpenWebUI, etc.)
- 🔒 Secure by Default — Auto-generated secrets and encrypted credentials
🏗️ Architecture
Built with Domain-Driven Design (DDD) principles for clean separation of concerns:
alfred/
├── agent/ # AI agent orchestration
│ ├── llm/ # LLM clients (Ollama, DeepSeek)
│ ├── tools/ # Tool implementations (api, filesystem, language)
│ └── workflows/ # YAML-defined multi-step workflows
├── application/ # Use cases & DTOs
│ ├── movies/ # Movie search
│ ├── torrents/ # Torrent management
│ └── filesystem/ # File operations (move, list, subtitles, seed links)
├── domain/ # Business logic & entities
│ ├── media/ # Release parsing
│ ├── movies/ # Movie entities
│ ├── tv_shows/ # TV show entities & value objects
│ ├── subtitles/ # Subtitle scanner, services, knowledge base
│ └── shared/ # Common value objects (ImdbId, FilePath, FileSize)
└── infrastructure/ # External services & persistence
├── api/ # External API clients (TMDB, qBittorrent, Knaben)
├── filesystem/ # File manager (hard-link based, path-traversal safe)
├── persistence/ # Three-tier memory (LTM/STM/Episodic) + JSON repositories
└── subtitle/ # Subtitle infrastructure
Key flows
Agent execution: agent.step(user_input) → LLM call → if tool_calls, execute each via registry → loop until no tool calls or max_tool_iterations → return final response.
Media organization workflow:
resolve_destination— Determines target folder/filename from release namemove_media— Hard-links file to library, deletes sourcemanage_subtitles— Scans, classifies, and places subtitle trackscreate_seed_links— Hard-links library file back to torrents/ for continued seeding
Memory tiers:
- LTM (
data/memory/ltm.json) — Persisted config, media library, watchlist - STM — Conversation history (capped at
MAX_HISTORY_MESSAGES) - Episodic — Transient search results, active downloads, recent errors
🚀 Quick Start
Prerequisites
- Python 3.14+
- uv (dependency manager)
- Docker & Docker Compose (recommended for full stack)
- API Keys:
- TMDB API key (get one here)
- Optional: DeepSeek or other LLM provider keys
Installation
# Clone the repository
git clone https://github.com/francwa/alfred_media_organizer.git
cd alfred_media_organizer
# Install dependencies
make install
# Install pre-commit hooks
make install-hooks
# Bootstrap environment (generates .env with secure secrets)
make bootstrap
# Validate your .env against the schema
make validate
# Edit .env with your API keys
nano .env
Running with Docker (Recommended)
# Start all services (LibreChat + Alfred + MongoDB + Ollama)
make up
# Or start with specific profiles
make up p=rag,meili # Include RAG and Meilisearch
make up p=qbittorrent # Include qBittorrent
make up p=full # Everything
# View logs
make logs
# Stop all services
make down
The web interface will be available at http://localhost:3080
Running Locally (Development)
uv run uvicorn alfred.app:app --reload --port 8000
⚙️ Configuration
Settings system
settings.toml is the single source of truth. The schema flows:
settings.toml → settings_schema.py → settings_bootstrap.py → .env + .env.make → settings.py
To add a setting: define it in settings.toml, run make bootstrap, then access via settings.my_new_setting.
# First time setup
make bootstrap
# Validate existing .env against schema
make validate
# Re-run after settings.toml changes (existing secrets preserved)
make bootstrap
Never commit .env or .env.make — both are gitignored and auto-generated.
Key settings (.env)
# --- CORE ---
MAX_HISTORY_MESSAGES=10
MAX_TOOL_ITERATIONS=10
# --- LLM ---
DEFAULT_LLM_PROVIDER=local # local (Ollama) | deepseek
OLLAMA_BASE_URL=http://ollama:11434
OLLAMA_MODEL=llama3.3:latest
LLM_TEMPERATURE=0.2
# --- API KEYS ---
TMDB_API_KEY=your-tmdb-key # Required for movie/show search
DEEPSEEK_API_KEY= # Optional
# --- SECURITY (auto-generated) ---
JWT_SECRET=<auto>
CREDS_KEY=<auto>
MONGO_PASSWORD=<auto>
🐳 Docker Services
Docker Profiles
| Profile | Extra services | Use case |
|---|---|---|
| (default) | — | LibreChat + Alfred + MongoDB + Ollama |
meili |
Meilisearch | Fast full-text search |
rag |
RAG API + VectorDB (PostgreSQL) | Document retrieval |
qbittorrent |
qBittorrent | Torrent downloads |
full |
All of the above | Complete setup |
make up # Start (default profile)
make up p=full # Start with all services
make down # Stop
make restart # Restart
make logs # Follow logs
make ps # Container status
🛠️ Available Tools
| Tool | Description |
|---|---|
find_media_imdb_id |
Search for movies/TV shows on TMDB by title |
find_torrent |
Search for torrents across multiple indexers |
get_torrent_by_index |
Get detailed info about a specific result |
add_torrent_by_index |
Download a torrent from search results |
add_torrent_to_qbittorrent |
Add a torrent via magnet link directly |
resolve_destination |
Compute the target library path for a release |
move_media |
Hard-link a file to its library destination |
manage_subtitles |
Scan, classify, and place subtitle tracks |
create_seed_links |
Prepare torrent folder so qBittorrent keeps seeding |
learn |
Teach Alfred a new pattern (release group, naming convention) |
set_path_for_folder |
Configure folder paths |
list_folder |
List contents of a configured folder |
set_language |
Set preferred language for the session |
💬 Usage Examples
Via Web Interface (LibreChat)
Navigate to http://localhost:3080 and start chatting:
You: Find Inception in 1080p
Alfred: I found 3 torrents for Inception (2010):
1. Inception.2010.1080p.BluRay.x264 (150 seeders) - 2.1 GB
2. Inception.2010.1080p.WEB-DL.x265 (80 seeders) - 1.8 GB
3. Inception.2010.1080p.REMUX (45 seeders) - 25 GB
You: Download the first one
Alfred: ✓ Added to qBittorrent! Download started.
You: Organize the Breaking Bad S01 download
Alfred: ✓ Resolved destination: /tv_shows/Breaking.Bad/Season 01/
✓ Moved 6 episode files
✓ Placed 6 subtitle tracks (fr, en)
✓ Seed links created in /torrents/
Via API
# Health check
curl http://localhost:8000/health
# Chat (OpenAI-compatible)
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "alfred",
"messages": [{"role": "user", "content": "Find The Matrix 4K"}]
}'
# List models
curl http://localhost:8000/v1/models
# View memory state
curl http://localhost:8000/memory/state
Alfred is compatible with any OpenAI-compatible client. Point it at http://localhost:8000/v1, model alfred.
🧠 Memory System
Alfred uses a three-tier memory system:
| Tier | Storage | Contents | Lifetime |
|---|---|---|---|
| LTM | JSON file (data/memory/ltm.json) |
Config, library, watchlist, learned patterns | Permanent |
| STM | RAM | Conversation history (capped) | Session |
| Episodic | RAM | Search results, active downloads, errors | Short-lived |
🧪 Development
Running Tests
# Run full suite (parallel)
make test
# Run with coverage report
make coverage
# Run a single file
uv run pytest tests/test_agent.py -v
# Run a single class
uv run pytest tests/test_agent.py::TestAgentInit -v
# Skip slow tests
uv run pytest -m "not slow"
Test coverage
The suite covers:
- Agent loop — tool execution, history, max iterations, error handling
- Tool registry — OpenAI schema format, parameter extraction
- Prompts — system prompt building, tool inclusion
- Memory — LTM/STM/Episodic operations, persistence
- Filesystem tools — path traversal security, folder listing
- File manager — hard-link, move, seed links (real filesystem, no mocks)
- Application use cases —
resolve_destination,create_seed_links,list_folder,move_media - Domain — TV show/movie entities, shared value objects (
ImdbId,FilePath,FileSize), subtitle scanner - Repositories — JSON-backed movie, TV show, subtitle repos
- Bootstrap — secret generation, idempotency, URI construction
- Workflows — YAML loading, structure validation
- Configuration — boundary validation for all settings
Code Quality
make lint # Ruff check --fix
make format # Ruff format + check --fix
Adding a New Tool
- Implement the function in
alfred/agent/tools/:
# alfred/agent/tools/api.py
def my_new_tool(param: str) -> dict[str, Any]:
"""Short description shown to the LLM to decide when to call this tool."""
memory = get_memory()
# ...
return {"status": "ok", "data": result}
- Register it in
alfred/agent/registry.py:
tool_functions = [
# ... existing tools ...
api_tools.my_new_tool,
]
The registry auto-generates the JSON schema from the function signature and docstring.
Adding a Workflow
Create a YAML file in alfred/agent/workflows/:
name: my_workflow
description: What this workflow does
steps:
- tool: resolve_destination
description: Find where the file should go
- tool: move_media
description: Move the file
Workflows are loaded automatically at startup.
Version Management
# Must be on main branch
make patch # 0.1.7 → 0.1.8
make minor # 0.1.7 → 0.2.0
make major # 0.1.7 → 1.0.0
📚 API Reference
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/v1/models |
List models (OpenAI-compatible) |
POST |
/v1/chat/completions |
Chat (OpenAI-compatible, streaming supported) |
GET |
/memory/state |
Full memory dump (debug) |
POST |
/memory/clear-session |
Clear STM + Episodic |
GET |
/memory/episodic/search-results |
Current search results |
🔧 Troubleshooting
Agent doesn't respond
- Check API keys in
.env - Verify the LLM is running:
docker logs alfred-ollama docker exec alfred-ollama ollama list - Check Alfred logs:
docker logs alfred-core
qBittorrent connection failed
- Verify qBittorrent is running:
docker ps | grep qbittorrent - Check credentials in
.env(QBITTORRENT_URL,QBITTORRENT_USERNAME,QBITTORRENT_PASSWORD)
Memory not persisting
- Check
data/directory is writable - Verify volume mounts in
docker-compose.yaml
Bootstrap fails
make validate # Check what's wrong with .env
make bootstrap # Regenerate (preserves existing secrets)
Tests failing
uv run pytest tests/test_failing.py -v --tb=long
🤝 Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes + add tests
- Run
make test && make lint && make format - Commit with Conventional Commits:
feat:,fix:,docs:,refactor:,test:,chore:,infra: - Open a Pull Request
📄 License
MIT License — see LICENSE file for details.
🙏 Acknowledgments
- LibreChat — Chat interface
- Ollama — Local LLM runtime
- DeepSeek — LLM provider
- TMDB — Movie & TV database
- qBittorrent — Torrent client
- FastAPI — Web framework
- uv — Fast Python package manager
Made with ❤️ by Francwa