Skip to content

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.pyYtDlpDownloader and request building - ytdlp.pyYtDlpAdapter for option construction - playlist.pyYtDlpPlaylistResolver

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 HOME to tmp_path so they never touch user config

See Testing.

See Also

  • Agent Guide — boundaries, constraints, and three wiring modes
  • Public API — what's stable vs. internal