Architecture¶
VDL is organized as a layered capability library with a clean facade (VDLClient) over stateless, testable services.
Design Philosophy¶
- Stateless by default — no persistence or logging unless explicitly wired
- Protocol-driven — all major components are interfaces (Protocols), enabling testing and swapping
- Dependency injection — services receive their dependencies at construction
- Thin adapters — CLI, MCP, and interactive app are thin adapters over core services, not independent implementations
Module Overview¶
| Module | Purpose |
|---|---|
__init__.py |
Public API surface (22 exports) |
client.py |
VDLClient facade |
runtime.py |
Component wiring and bootstrap |
models.py |
Core dataclasses and enums |
execution.py |
DownloadExecutionService |
downloader.py |
Downloader protocol and YtDlpDownloader impl |
playlist.py |
PlaylistService and YtDlpPlaylistResolver |
planner.py |
InputPlanner — input classification |
input_parser.py |
Raw input parsing (link files, multi-URL strings) |
probe.py |
Remote URL probing for plan_with_probe() |
selection.py |
Playlist and discovery item selection logic |
urls.py |
URL normalization and classification utilities |
discovery.py |
DiscoveryService — page crawling |
acquisition.py |
Plan structures (AcquisitionPlan, AcquisitionItem) |
crawler.py |
SiteRuleDiscoveryResolver — rule-based crawling |
site_rules.py |
Site crawling rules loader (SiteRule, load_site_rules()) |
ytdlp.py |
YtDlpAdapter — yt-dlp options builder |
impersonation.py |
curl-cffi browser impersonation for bot-detection bypass |
error_translation.py |
Translates raw yt-dlp/network errors into VDL exceptions |
naming.py |
Output filename and path planning (FilenamePlan) |
config.py |
VDLConfig, TomlConfigStore |
state.py |
StateStore protocol, SQLiteStateStore impl |
paths.py |
VDLPaths — default directory resolution |
progress.py |
DownloadProgress, RichProgressEmitter |
cancellation.py |
CancellationToken, CancellationController |
logging.py |
Logger protocol, NullLogger impl |
event_log.py |
FileEventLogger — audit logging |
bootstrap.py |
ensure_app_ready() — first-run setup |
doctor.py |
Doctor service — health checks |
errors.py |
Exception hierarchy (10 exceptions) |
cli.py |
CLI entry point and subcommands |
interactive.py |
Interactive mode (REPL, prompts, Rich UI) |
prompt_utils.py |
Interactive prompt helpers |
mcp_server.py |
MCP server entry point |
mcp_setup.py |
MCP client config generation (vdl mcp setup) |
tools.py |
Stateless tool functions |
serialization.py |
JSON serialization helpers |
Layered Architecture¶
graph TB
subgraph Consumers["User Interfaces"]
CLI["CLI<br/>(vdl)"]
Interactive["Interactive App"]
MCP["MCP Server<br/>(vdl-mcp)"]
Library["Python Library"]
end
subgraph Facade["Main Facade"]
Client["VDLClient<br/>(orchestration)"]
end
subgraph Services["Services (Business Logic)"]
Planner["InputPlanner"]
Execution["DownloadExecutionService"]
Playlist["PlaylistService"]
Discovery["DiscoveryService"]
Doctor["Doctor"]
end
subgraph Infrastructure["Infrastructure (Adapters & Storage)"]
Downloader["YtDlpDownloader"]
Resolver["YtDlpPlaylistResolver"]
Crawler["SiteRuleDiscoveryResolver"]
State["SQLiteStateStore<br/>(opt-in)"]
Config["TomlConfigStore"]
Logger["FileEventLogger<br/>(opt-in)"]
end
subgraph External["External Dependencies"]
ytdlp["yt-dlp"]
ffmpeg["ffmpeg<br/>ffprobe"]
end
CLI --> Client
Interactive --> Client
MCP --> Client
Library --> Client
Client --> Planner
Client --> Execution
Client --> Playlist
Client --> Discovery
Client --> Doctor
Execution --> Downloader
Execution --> State
Playlist --> Resolver
Discovery --> Crawler
Planner --> Resolver
Downloader --> ytdlp
Resolver --> ytdlp
Downloader --> ffmpeg
Protocols (Interfaces)¶
All major components are defined as Protocols, enabling testing and alternative implementations:
| Protocol | Implementations | Purpose |
|---|---|---|
Downloader |
YtDlpDownloader |
Media extraction |
PlaylistResolver |
YtDlpPlaylistResolver |
Playlist resolution |
StateStore |
SQLiteStateStore, InMemoryStateStore (tests) |
Persistence |
ConfigStore |
TomlConfigStore |
Config loading |
Logger |
NullLogger, FileEventLogger |
Event logging |
DiscoveryResolver |
SiteRuleDiscoveryResolver |
Page crawling |
Tests use fake implementations:
- FakeDownloader — configurable results
- FakePlaylistResolver — configurable items
- InMemoryStateStore — in-process storage
Service Classes¶
VDLClient¶
Main orchestration facade. Delegates to four services:
- planner: InputPlanner
- execution: DownloadExecutionService
- playlist_service: PlaylistService
- discovery_service: DiscoveryService
No business logic in the client itself; purely a coordinating facade.
InputPlanner¶
Classifies raw user input and builds AcquisitionPlans:
- Detects URLs, playlists, files, multi-URL lists
- No network calls (except for remote inspection via plan_with_probe)
- Returns plan with metadata
DownloadExecutionService¶
Executes downloads:
- Builds requests from URLs
- Calls Downloader for each download
- Records state (if StateStore is wired)
- Logs events (if Logger is wired)
PlaylistService¶
Handles playlist operations:
- Resolves via PlaylistResolver
- Handles concurrent downloads with semaphore
- Isolates individual item failures
DiscoveryService¶
Crawls pages for media:
- Uses DiscoveryResolver (site-rule or generic crawler)
- Emits progress callbacks
- Returns AcquisitionPlan with discovered items
Doctor¶
Health checks: - Verifies ffmpeg, ffprobe, yt-dlp - Checks config and state file accessibility - Reports on JS runtime, MCP availability
Dependency Injection¶
Services receive their dependencies at construction:
execution = DownloadExecutionService(
config=config,
downloader=downloader,
state_store=state_store, # Optional; None for stateless
logger=logger # Optional; NullLogger for headless
)
This pattern enables: - Testing with mocks/fakes - Swapping implementations (e.g., different StateStore) - Clean separation of concerns
Stateless vs Stateful Wiring¶
Stateless (Library Mode)¶
from vdl.runtime import build_library_components
components = build_library_components()
# state_store=None, logger=NullLogger
# No SQLite, no file logging
# No app directories required
Used by: headless CLI, MCP server, VDLClient() bare init
Stateful (Interactive Mode)¶
from vdl.runtime import build_interactive_components
components = build_interactive_components()
# state_store=SQLiteStateStore, logger=FileEventLogger
# SQLite at ~/.local/state/vdl/vdl.db
# Audit logs at ~/.local/state/vdl/logs/
Used by: interactive app (vdl / vdl interactive)
Boundary Between VDL and yt-dlp¶
All yt-dlp integration is isolated in:
- downloader.py — YtDlpDownloader and request building
- ytdlp.py — YtDlpAdapter for option construction
- playlist.py — YtDlpPlaylistResolver
No downstream code calls raw yt-dlp APIs. This maintains a clean boundary and makes it easy to adapt or replace the downloader in the future.
Error Handling¶
10 exception types in the hierarchy, all inherit from VDLError:
VDLError
├── VDLConfigError
├── VDLInputError
├── VDLDownloadError
├── VDLPlaylistError
├── VDLStateError
├── VDLDependencyError
├── VDLSelectionError
├── VDLCrawlerError
└── VDLCancelled (internal)
Services raise the appropriate exception type. Callers catch by type to distinguish failure modes.
Testing¶
- Unit tests — mock/fake all dependencies, test service logic in isolation
- Contract tests — validate fake implementations honor protocol invariants
- Integration tests — real SQLiteStateStore, real filesystem, real yt-dlp
- HOME isolation — all tests redirect
HOMEtotmp_pathso they never touch user config
See Testing.
See Also¶
- Agent Guide — boundaries, constraints, and three wiring modes
- Public API — what's stable vs. internal