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.