Skip to content

Downloads

Single Video Download

The most common operation: download one video at a chosen quality.

CLI

vdl download "https://example.com/video" --quality 720p

Python

import asyncio
from vdl import VDLClient
from vdl.models import Quality

async def main():
    client = VDLClient()
    result = await client.download(
        "https://example.com/video",
        quality=Quality.P720
    )
    print(result.status)      # DownloadStatus.COMPLETED
    print(result.output_path)  # /path/to/file.mp4
    print(result.error)        # None if successful

asyncio.run(main())

Quality Selection

Choose from these quality levels (mapped to yt-dlp format selectors):

Level Description Notes
best Highest available Default if not specified
2160p 4K resolution Not always available
1440p 2K resolution Not always available
1080p Full HD Most sources have this
720p HD Very common fallback
480p Standard definition Smaller files
medium Mid-range (usually ~480p) Safe default for slow connections
low Minimum acceptable Highly compressed
audio Extract audio only (mp3/m4a) No video track

CLI flag: --quality or -q

vdl download "URL" --quality audio
vdl download "URL" -q 1080p

Python parameter: quality: Quality (use vdl.models.Quality enum)

from vdl.models import Quality

result = await client.download("URL", quality=Quality.AUDIO)
result = await client.download("URL", quality=Quality.P1080)

Output Collision Policy

When the target file already exists, VDL uses the IfExists policy to decide what to do:

Policy Action When to use
rename Download with a numbered suffix (e.g., video.1.mp4) Default. Safe if you want to keep all versions
skip Return SKIPPED status; do not download Re-running the same command should be idempotent
overwrite Delete the existing file and re-download Force-refresh a file
fail Return FAILED status; do not download Treat existing files as errors in automation

CLI flag: --if-exists (or hidden --force alias for overwrite)

# Skip if file exists
vdl download "URL" --if-exists skip

# Rename (default)
vdl download "URL" --if-exists rename

# Delete and re-download
vdl download "URL" --if-exists overwrite

# Fail if exists
vdl download "URL" --if-exists fail

Python parameter: if_exists: IfExists (default IfExists.RENAME)

from vdl.models import IfExists

result = await client.download("URL", if_exists=IfExists.SKIP)
result = await client.download("URL", if_exists=IfExists.OVERWRITE)

Skip Behavior

When if_exists=IfExists.SKIP and a file is found, the result is:

result.status == DownloadStatus.SKIPPED
result.skip_reason == SkipReason.ALREADY_PRESENT

This is not an error — it is a normal outcome of collision handling. Check result.status to distinguish completed vs. skipped in your workflow.

Cookies & Authentication

Some sites require authentication or bot-detection workarounds.

Using Cookies File

Export cookies from your browser as Netscape format, then pass the file:

vdl download "URL" --cookies ~/Downloads/cookies.txt

Python:

result = await client.download("URL", cookies_file="~/Downloads/cookies.txt")

Using Browser Cookies

VDL can read cookies directly from your browser:

vdl download "URL" --cookies-from-browser firefox

Supported browsers: firefox, chrome, chromium, edge, safari

Python:

result = await client.download("URL", cookies_from_browser="firefox")

Media Options

Fine-tune what gets included in the output:

Thumbnails

Write thumbnail image as a sidecar file (video.jpg):

Interactive: choose Save thumbnail? when prompted.

Python: pass write_thumbnail=True.

Embedding in the video file itself (preferred when possible):

# In config: embed_thumbnail = true  (default)

Subtitles

Write subtitle files alongside the video:

Interactive: choose Save subtitles (.vtt)? when prompted.

Choose subtitle languages in Python with comma-separated language codes.

Python:

result = await client.download(
    "URL",
    write_subtitles=True,
    subtitle_langs="en,fr"
)

Download Status Lifecycle

A download progresses through these statuses:

Status Meaning
QUEUED Waiting to start
STARTING Initialization (analyzing URL, setting up)
DOWNLOADING Actively fetching media
PROCESSING Post-download (muxing, metadata embedding)
COMPLETED Success — file is ready
SKIPPED Collision policy returned skip (not an error)
FAILED Error (network, invalid URL, missing dependency, etc.)
CANCELLED User cancelled via cancellation token

Check the result:

result = await client.download("URL")

if result.status == DownloadStatus.COMPLETED:
    print(f"Done: {result.output_path}")
elif result.status == DownloadStatus.SKIPPED:
    print(f"Already present: {result.output_path}")
elif result.status == DownloadStatus.FAILED:
    print(f"Error: {result.error}")
elif result.status == DownloadStatus.CANCELLED:
    print("User cancelled")

DownloadResult Fields

@dataclass(frozen=True)
class DownloadResult:
    id: str                              # Unique download ID
    source_url: str                      # Original URL
    status: DownloadStatus               # Final status
    quality: Quality | None              # Requested quality
    request_key: str | None              # Stable "url::quality" key
    output_path: Path | None             # Path to saved file
    error: str | None                    # Error message if failed
    skipped_reason: str | None           # Human-readable skip reason
    skip_reason: SkipReason | None       # Structured skip classification
    metadata: DownloadMetadata           # Typed media metadata (see below)
    error_category: str | None           # Machine-readable error category

DownloadMetadata

result.metadata is a typed DownloadMetadata instance. All fields are optional:

m = result.metadata

# Content identity
print(m.title)
print(m.channel)
print(m.uploader)
print(m.duration_sec)          # float seconds

# Provider info
print(m.provider)              # e.g. "youtube"
print(m.provider_item_id)

# Playlist context (populated when downloading as part of a playlist)
print(m.playlist_title)
print(m.playlist_index)        # 1-based position in playlist
print(m.playlist_count)        # total items in playlist
print(m.playlist_uploader)
print(m.playlist_uploader_id)

# Local output
print(m.downloaded_path)       # absolute path string to media file
print(m.sidecars.thumbnail)    # Path | None — thumbnail sidecar
print(m.sidecars.subtitles)    # dict[str, Path] — lang code → subtitle file

DownloadSidecars

Written alongside the video when write_thumbnail=True or write_subtitles=True:

result = await client.download(
    "URL",
    write_thumbnail=True,
    write_subtitles=True,
    subtitle_langs="en,fr",
)

sidecars = result.metadata.sidecars
if sidecars.thumbnail:
    print(f"Thumbnail: {sidecars.thumbnail}")

for lang, path in sidecars.subtitles.items():
    print(f"Subtitle [{lang}]: {path}")

Progress Monitoring

Track download progress in real time using on_progress= per call, or subscribe to client.progress to receive events from all downloads on the client:

# Per-call callback
async def on_progress(progress: DownloadProgress):
    print(f"{progress.percent}% — {progress.title} @ {progress.speed_bps / 1e6:.1f} Mbps")

result = await client.download("URL", on_progress=on_progress)

# Client-wide event subscription
async def handler(p: DownloadProgress) -> None:
    print(f"[{p.status.value}] {p.title}: {p.percent}%")

client.progress += handler     # fires for every download on this client

See Progress & Cancellation for full details.

Migration Note

Previous versions used force=True. This has been replaced by the IfExists collision policy:

  • Old: force=True → New: if_exists=IfExists.OVERWRITE
  • Old: force=False → New: if_exists=IfExists.RENAME (default)

The CLI still accepts a hidden --force flag for backward compatibility, but the documented way to force overwrite is:

vdl download "URL" --if-exists overwrite

Examples

See demos/download_video.py for a complete runnable example.