Skip to content

Python API

VDL exposes a clean async API via VDLClient and helper functions in vdl.tools.

This developer documentation is published live at: https://video-downloader-657d90.gitlab.io/python-api/

VDLClient

The main entry point for programmatic access.

Construction

from vdl import VDLClient

# Stateless by default; loads config from ~/.config/vdl/config.toml
client = VDLClient()

# With custom config
from vdl import VDLConfig
from pathlib import Path

config = VDLConfig(
    output_dir=Path("~/Videos"),
    default_quality="1080p",
    max_concurrent_downloads=2
)
client = VDLClient(config=config)

# Optional crawl defaults for discovery plans only
from vdl import CrawlerPolicy
client = VDLClient(
    config=config,
    crawler_policy=CrawlerPolicy(first_successful=3),
)

Core Methods

Method Returns Description
download(url, ...) DownloadResult Download a single URL
download_playlist(url, ...) list[DownloadResult] Download all playlist items
resolve_playlist(url) PlaylistInfo Inspect playlist without downloading
plan(input, ...) AcquisitionPlan Classify input and build a plan
plan_with_probe(input, ...) AcquisitionPlan Plan with network inspection
discover(url, ..., on_status, crawler_policy) AcquisitionPlan Crawl a page for media
execute_plan(plan, ...) list[DownloadResult] Execute a plan
execute_batch(plan, ...) BatchSummary Execute and get summary stats
status(id=None) StatusRecord or list or None Query download history
doctor() DoctorReport Health check

Common Parameters

Many methods accept these optional parameters:

result = await client.download(
    url="https://example.com/video",
    output_dir="~/Videos",                    # Override config
    quality=Quality.P720,                     # From vdl
    if_exists=IfExists.RENAME,                # From vdl
    on_progress=callback,                     # DownloadProgress → None (per-call)
    cancel_token=token,                       # CancellationToken
    write_thumbnail=True,                     # Write .jpg sidecar
    write_subtitles=True,                     # Write subtitle files
    subtitle_langs="en,fr"                    # Comma-separated codes
)

Progress Event (client-wide)

client.progress is an Event[DownloadProgress] that fires for every progress update across all downloads. Subscribe once and receive progress from all download(), download_playlist(), and execute_plan() calls:

async def my_handler(p: DownloadProgress) -> None:
    print(f"[{p.status.value}] {p.title}: {p.percent}%")

client.progress += my_handler            # subscribe
# ... run downloads ...
client.progress -= my_handler            # unsubscribe when done

on_progress= on individual calls still works and takes priority over the client-level event for that specific call.

Models and Enums

All exported via vdl or vdl.models:

Enums

from vdl.models import Quality, IfExists, DownloadStatus, SkipReason

# Quality options
Quality.BEST, Quality.P1080, Quality.P720, Quality.AUDIO, ...

# Collision policy
IfExists.RENAME, IfExists.SKIP, IfExists.OVERWRITE, IfExists.FAIL

# Download status
DownloadStatus.COMPLETED, SKIPPED, FAILED, CANCELLED, ...

# Skip reason
SkipReason.ALREADY_PRESENT  # Only value

Dataclasses

from vdl import (
    DownloadResult, DownloadMetadata, DownloadSidecars,
    PlaylistInfo, PlaylistItem,
    DownloadRequest, AcquisitionPlan, AcquisitionItem,
    BatchSummary, CrawlerDecision, CrawlerPolicy,
    DoctorReport, ReconciliationReport,
    DownloadProgress,
)

DownloadResult.metadata is a typed DownloadMetadata instance:

result = await client.download("URL")

# Typed access — no key lookups needed
print(result.metadata.title)
print(result.metadata.channel)
print(result.metadata.duration_sec)
print(result.metadata.playlist_title)
print(result.metadata.playlist_count)

# Sidecar files written alongside the video
print(result.metadata.sidecars.thumbnail)   # Path | None
print(result.metadata.sidecars.subtitles)   # dict[str, Path] — lang → file

CrawlerPolicy applies only to discovery plans. Use it to control crawl admission and success-target execution without affecting direct downloads:

from vdl import CrawlerPolicy, VDLClient

client = VDLClient(crawler_policy=CrawlerPolicy(first_successful=2))

plan = await client.discover(
    "https://example.com/gallery",
    crawler_policy=CrawlerPolicy(min_filesize_bytes=10 * 1024 * 1024),
)

results = await client.execute_plan(
    plan,
    crawler_policy=CrawlerPolicy(first_successful=3),
)

expanded = await client.discover(
    "https://example.com/gallery",
    crawler_policy=CrawlerPolicy(crawl_pagination_pages=True),
)

Exception Hierarchy

from vdl import VDLError

# All exceptions inherit from VDLError
VDLError              # Base
├── VDLConfigError      # Config load/save/validation
├── VDLInputError       # User input parsing
├── VDLDownloadError    # Download operation
├── VDLPlaylistError    # Playlist resolution
├── VDLStateError       # Persistent state
├── VDLDependencyError  # Missing ffmpeg, yt-dlp, etc.
├── VDLSelectionError   # Playlist selection
├── VDLCrawlerError     # Page crawling
└── VDLCancelled        # (Internal; don't catch)

Catch as needed:

try:
    result = await client.download("URL")
except VDLConfigError:
    print("Bad config")
except VDLDownloadError as e:
    print(f"Download failed: {e}")

Logger Interface

Inject a custom logger to capture events:

from vdl import Logger

class MyLogger(Logger):
    def log(self, level: str, event: str, **kwargs) -> None:
        print(f"[{level}] {event}: {kwargs}")

client = VDLClient(logger=MyLogger())

Or use the built-in NullLogger (default, no output).

vdl.tools — Transport-Agnostic Functions

Thin async functions that delegate to VDLClient. Useful for MCP, RPC, or other integrations:

from vdl.tools import download_video, resolve_playlist, get_status, doctor

# Each has an async variant and a sync variant
result = await download_video(
    url="...",
    quality="720p",
    output_dir="~/Videos",
    if_exists="rename",
    client=client
)

# Sync variant (calls asyncio.run internally)
result = download_video_sync(
    url="...",
    quality="720p",
    output_dir="~/Videos",
    client=client
)

Available tools: - download_video() / download_video_sync() - plan_download() / plan_download_sync() - resolve_playlist() / resolve_playlist_sync() - get_status() / get_status_sync() - doctor() / doctor_sync()

Import Guide

# Main client
from vdl import VDLClient

# Config
from vdl import VDLConfig

# Models and enums (all exported via __init__)
from vdl import (
    DownloadResult, DownloadMetadata, DownloadSidecars,
    DownloadStatus, SkipReason, Quality, IfExists,
    PlaylistInfo, PlaylistItem,
    DownloadProgress,
    AcquisitionPlan, AcquisitionItem, PlanKind,
    BatchSummary, DoctorReport,
    ReconciliationEntry, ReconciliationReport,
)

# Exceptions
from vdl import VDLError, VDLDownloadError, VDLConfigError

# Utilities
from vdl.cancellation import CancellationToken, CancellationController
from vdl import DownloadProgress

# Tools (stateless functions)
from vdl.tools import download_video, resolve_playlist, doctor

# Logger
from vdl import Logger, NullLogger

Complete Example

import asyncio
from vdl import VDLClient, VDLConfig, IfExists, Quality, DownloadProgress, DownloadStatus
from vdl.cancellation import CancellationToken
from pathlib import Path

async def main():
    client = VDLClient()
    token = CancellationToken()

    # Client-wide progress: subscribe once, receives events from all downloads
    async def on_progress(p: DownloadProgress) -> None:
        if p.percent is not None:
            print(f"  [{p.status.value}] {p.title}: {p.percent}%")

    client.progress += on_progress

    # Single video
    result = await client.download(
        "https://example.com/video",
        quality=Quality.P720,
        if_exists=IfExists.SKIP,
        write_thumbnail=True,
        cancel_token=token,
    )

    if result.status == DownloadStatus.COMPLETED:
        print(f"Done: {result.output_path}")
        print(f"Title: {result.metadata.title}")
        print(f"Channel: {result.metadata.channel}")
        if result.metadata.sidecars.thumbnail:
            print(f"Thumbnail: {result.metadata.sidecars.thumbnail}")
    elif result.status == DownloadStatus.SKIPPED:
        print(f"Already present: {result.output_path}")
    else:
        print(f"Error: {result.error}")

    # Playlist — first 3 items only
    results = await client.download_playlist(
        "https://example.com/playlist",
        first_n=3,
        quality=Quality.P1080,
        write_subtitles=True,
        subtitle_langs="en",
    )

    for r in results:
        status = "✓" if r.status == DownloadStatus.COMPLETED else "✗"
        print(f"{status} {r.metadata.title}{r.metadata.playlist_index}/{r.metadata.playlist_count}")

    client.progress -= on_progress

asyncio.run(main())

Working Examples

See the demos/ directory: - download_video.py — single video - download_playlist.py — full playlist - resolve_playlist.py — read-only inspection - download_link_file.py — batch from file - crawl_websites.py — discovery workflow

See Also