Skip to content

Advanced Patterns

Production-grade workflows for handling complex scenarios: duplicate detection, error recovery, batch operations, and failure handling.

Important: The VDL library is stateless by default. It does not create databases, logs, or implicit state. Your application owns all persistence, history, and state management. These patterns show how to build robust workflows on top of VDL's stateless execution layer.

Duplicate Detection & Collision Handling

VDL detects when media already exists and applies a collision policy to handle conflicts.

Understanding Duplicate Detection

The model: If a completed media file exists at the target path, the request should be skipped.

VDL recognizes: - Completed media.mp4, .webm, .mkv, etc. - In-flight files.part, .ytdl, .tmp, .aria2 (continue/resume) - Sidecar files.vtt, .srt, .jpg, .json (not media, don't count as "downloaded")

Browser/Finder copies are recognized as duplicates:

Original:  episode.mp4
Matches:   episode (1).mp4  ← Browser copy counter
           episode (2).mp4  ← Finder copy format
           episode copy.mp4 ← Finder copy format

Does NOT match:
           episode 2.mp4    ← Real title, not a copy
           episode-v2.mp4   ← Different version

Collision Policies

When a file exists, use if_exists to choose behavior:

Policy Behavior When to Use
SKIP Return SKIPPED without downloading Archives, incremental jobs
RENAME Save as title-2.mp4, title-3.mp4, etc. Batch collection jobs
OVERWRITE Replace the existing file Re-downloads, updates
FAIL Raise VDLDownloadError Strict workflows, error tracking

Example: Batch with Smart Deduplication

import asyncio
from pathlib import Path
from vdl import VDLClient, IfExists
from vdl.models import Quality

async def batch_with_dedup():
    client = VDLClient()
    output_dir = Path("~/Downloads/VDL").expanduser()

    urls = [
        "https://example.com/video1",
        "https://example.com/video2",
        "https://example.com/video1",  # Duplicate URL!
    ]

    # Create a plan from URLs
    plan = await client.plan(urls)
    print(f"Planned {len(plan.items)} items")

    # Execute with deduplication
    # - Duplicate URLs will share the same output file
    # - First download completes, second is skipped
    results = await client.execute_plan(
        plan,
        quality=Quality.P720,
        if_exists=IfExists.SKIP,  # Skip if file exists
    )

    completed = [r for r in results if r.status.value == "completed"]
    skipped = [r for r in results if r.status.value == "skipped"]

    print(f"Downloaded: {len(completed)}")
    print(f"Skipped (duplicates): {len(skipped)}")

asyncio.run(batch_with_dedup())

Preflight Duplicate Check

Check before downloading using the duplicate detection service:

from vdl import DownloadRequest, VDLClient
from vdl.models import Quality
from pathlib import Path

async def preflight_check():
    # Library mode: stateless by default
    client = VDLClient()

    request = DownloadRequest(
        id="check-1",
        source_url="https://example.com/video",
        quality=Quality.P720,
        output_dir=Path("~/Downloads/VDL").expanduser(),
    )

    # Check if this would be a duplicate
    decision = await client.execution_service.duplicate_detection.check(request)

    print(f"Action: {decision.action}")          # "skip" or "continue"
    print(f"Reason: {decision.reason}")          # why
    print(f"Target path: {decision.target_path}") # where it would go

    # Now decide whether to download
    if decision.action == "skip":
        print("File already exists, skipping")
    else:
        result = await client.download(request.source_url)

asyncio.run(preflight_check())

Error Handling & Recovery

Handle download failures, implement retry logic, and recover gracefully.

Error Types & Classification

VDL provides specific exception types for different failure modes:

from vdl import (
    VDLError,              # Base class
    VDLConfigError,        # Config load/validation
    VDLInputError,         # User input parsing
    VDLDownloadError,      # Download operation
    VDLDependencyError,    # Missing ffmpeg, yt-dlp
    VDLStateError,         # Persistent state issues
    VDLCrawlerError,       # Page crawling
)

async def handle_errors():
    # Library mode: stateless, handle exceptions from your code
    client = VDLClient()

    try:
        result = await client.download("https://example.com/video")
    except VDLDependencyError:
        print("Install ffmpeg and yt-dlp first")
        print("  macOS: brew install ffmpeg yt-dlp")
        print("  Linux: sudo apt-get install ffmpeg && pip install yt-dlp")
    except VDLDownloadError as e:
        print(f"Download failed: {e}")
    except VDLError as e:
        print(f"VDL error: {e}")

Result-Based Error Checking

Most operations return a result with status, not exceptions:

from vdl.models import DownloadStatus

async def check_result():
    client = VDLClient()
    result = await client.download("https://example.com/video")

    if result.status == DownloadStatus.COMPLETED:
        print(f"✓ Downloaded to {result.output_path}")

    elif result.status == DownloadStatus.SKIPPED:
        print(f"⊘ Already present: {result.output_path}")

    elif result.status == DownloadStatus.FAILED:
        print(f"✗ Failed: {result.error}")

        # Check if retryable
        if result.metadata.get("retryable"):
            strategy = result.metadata.get("suggested_retry")
            print(f"Suggestion: {strategy}")

    elif result.status == DownloadStatus.CANCELLED:
        print("Cancelled by user")

Quality Fallback Strategy

Try multiple qualities before failing:

from vdl.models import Quality, DownloadStatus

async def download_with_fallback():
    client = VDLClient()
    url = "https://example.com/video"

    # Fallback chain: prefer high quality, accept lower
    qualities = [Quality.P1080, Quality.P720, Quality.P480, Quality.AUDIO]

    for quality in qualities:
        print(f"Trying {quality.value}...", end=" ")
        result = await client.download(url, quality=quality)

        if result.status == DownloadStatus.COMPLETED:
            print(f"✓ Success!")
            return result
        elif result.status == DownloadStatus.SKIPPED:
            print(f"⊘ Already present")
            return result
        else:
            print(f"✗ Failed")

    raise Exception("All quality levels failed")

asyncio.run(download_with_fallback())

Exponential Backoff Retry

Implement smart retry with increasing delays:

import asyncio
from vdl.models import DownloadStatus

async def retry_with_backoff():
    client = VDLClient()
    url = "https://example.com/video"
    max_retries = 3
    base_delay = 1  # seconds

    for attempt in range(max_retries):
        print(f"Attempt {attempt + 1}/{max_retries}...", end=" ")
        result = await client.download(url)

        if result.status in (DownloadStatus.COMPLETED, DownloadStatus.SKIPPED):
            print("✓ Success!")
            return result

        if not result.metadata.get("retryable"):
            print("✗ Not retryable")
            return result

        if attempt < max_retries - 1:
            delay = base_delay * (2 ** attempt)  # 1s, 2s, 4s, ...
            print(f"✗ Failed, retrying in {delay}s...")
            await asyncio.sleep(delay)

    print(f"✗ Failed after {max_retries} attempts")
    return result

asyncio.run(retry_with_backoff())

Batch Operations with Failure Isolation

Download multiple items while isolating failures — one failure doesn't stop the batch.

Basic Batch

async def batch_download():
    client = VDLClient()

    urls = [
        "https://example.com/video1",
        "https://example.com/video2",
        "https://example.com/video3",
    ]

    # Plan the batch
    plan = await client.plan(urls)

    # Execute: each item fails independently
    results = await client.execute_plan(plan, quality=Quality.P720)

    # Analyze results
    completed = [r for r in results if r.status.value == "completed"]
    failed = [r for r in results if r.status.value == "failed"]

    print(f"Completed: {len(completed)}/{len(results)}")
    print(f"Failed: {len(failed)}/{len(results)}")

    # Don't stop on first failure — report all
    for result in failed:
        print(f"✗ {result.source_url}: {result.error}")

Batch Summary

Get statistics without iterating:

async def batch_summary():
    client = VDLClient()

    plan = await client.plan([...])

    # execute_batch returns a summary, not individual results
    summary = await client.execute_batch(plan, quality=Quality.P720)

    print(f"✓ Completed: {summary.completed}")
    print(f"⊘ Skipped: {summary.skipped}")
    print(f"✗ Failed: {summary.failed}")
    print(f"⏹ Cancelled: {summary.cancelled}")
    print(f"Time: {summary.elapsed_seconds}s")

Retry Failures from Batch

Use the retry helpers:

from vdl.core.retry import retryable_failed_results, suggested_retry_strategy

async def batch_with_retry():
    client = VDLClient()

    # Initial batch
    plan = await client.plan([...])
    results = await client.execute_plan(plan, quality=Quality.P720, write_subtitles=True)

    # Find retryable failures
    failed = retryable_failed_results(results)
    if not failed:
        print("No retryable failures")
        return results

    # Get the most common suggested retry
    strategy = suggested_retry_strategy(results)
    print(f"Retrying {len(failed)} items: {strategy}")

    # Retry based on suggested strategy
    if strategy == "download without subtitles":
        retry_results = await client.execute_plan(
            plan,
            quality=Quality.P720,
            write_subtitles=False,
        )
        return retry_results

    return results

asyncio.run(batch_with_retry())

Adding State to the Stateless Library

The VDL library is stateless by default — it doesn't create databases, logs, or app directories. Your application owns all persistence decisions.

When to add state: - Building a persistent download history - Tracking which URLs have been attempted - Storing user preferences - Maintaining a media library catalog

How to add state: 1. Build your own database/state model 2. Use VDL's stateless API 3. Save results to your state after each operation 4. Query your state before downloading (for deduplication)

Example:

import asyncio
from vdl import VDLClient, DownloadStatus

class YourDownloadService:
    def __init__(self, db_path: str):
        self.client = VDLClient()  # stateless
        self.db = YourDatabase(db_path)  # your persistence layer

    async def download(self, url: str):
        # Check your state before downloading
        if self.db.has_completed(url):
            return "Already downloaded"

        # Use stateless VDL
        result = await self.client.download(url)

        # Save result to your state
        if result.status == DownloadStatus.COMPLETED:
            self.db.record_download(url, result.output_path)

        return result

asyncio.run(YourDownloadService("/path/to/db").download("..."))


Using Interactive Mode (With Built-In State)

When you need history/state built-in, use interactive mode instead of the library:

Query Download History

from vdl.runtime import build_interactive_components

async def check_history():
    # Interactive mode wires SQLiteStateStore
    components = build_interactive_components()
    client = VDLClient(state_store=components.state_store)

    # Query all downloads
    all_records = await client.status()
    print(f"Total downloads: {len(all_records)}")

    # Query specific download
    record = await client.status(id="some-download-id")
    if record:
        print(f"Status: {record.status}")
        print(f"Downloaded: {record.completed_at}")
        print(f"Path: {record.output_path}")

asyncio.run(check_history())

Reconciliation: Detect Stale State

When disk and DB are out of sync:

async def reconcile_state():
    components = build_interactive_components()
    client = VDLClient(state_store=components.state_store)

    # Compare DB state with filesystem
    report = await client.reconciliation()

    print(f"Missing files: {len(report.missing)}")
    for entry in report.missing:
        print(f"  ✗ {entry.output_path} (DB says downloaded, but missing on disk)")

    print(f"Orphaned files: {len(report.orphaned)}")
    for entry in report.orphaned:
        print(f"  ? {entry} (on disk, but not in DB)")

asyncio.run(reconcile_state())

Common Production Patterns

Pattern 1: Incremental Archive

Download new items into an archive, skip previously downloaded:

async def incremental_archive():
    client = VDLClient()
    archive_dir = Path("/media/archive")

    # New URLs to check
    urls = [...]

    # Execute with SKIP: only download new items
    plan = await client.plan(urls)
    results = await client.execute_plan(
        plan,
        output_dir=archive_dir,
        if_exists=IfExists.SKIP,  # Don't re-download
    )

    completed = len([r for r in results if r.status.value == "completed"])
    skipped = len([r for r in results if r.status.value == "skipped"])

    print(f"Added {completed} new items")
    print(f"Already have {skipped} items")

Pattern 2: Strict Batch (Fail on Collision)

For pipelines where duplicates are unexpected:

async def strict_batch():
    client = VDLClient()

    plan = await client.plan(urls)

    try:
        results = await client.execute_plan(
            plan,
            if_exists=IfExists.FAIL,  # Error if file exists
        )
        print("✓ All downloads succeeded (no duplicates found)")
    except VDLDownloadError as e:
        print(f"✗ Collision detected: {e}")
        # Handle: maybe archive the batch first, retry later

Pattern 3: Graceful Degradation

Try best quality, fall back to audio if video fails:

async def graceful_degradation():
    client = VDLClient()
    url = "https://example.com/video"

    # Try video at various qualities
    for quality in [Quality.P1080, Quality.P720, Quality.P480]:
        result = await client.download(url, quality=quality)
        if result.status == DownloadStatus.COMPLETED:
            return result

    # Fall back to audio
    result = await client.download(url, quality=Quality.AUDIO)
    if result.status == DownloadStatus.COMPLETED:
        print("✓ Downloaded audio (video unavailable)")
        return result

    # Give up
    raise Exception("Unable to download at any quality")

Pattern 4: Rate-Limited Batch

Process items with delays to avoid rate limiting:

import asyncio

async def rate_limited_batch():
    client = VDLClient()
    urls = [...]
    delay_between = 5  # seconds

    for i, url in enumerate(urls):
        if i > 0:
            print(f"Waiting {delay_between}s before next item...")
            await asyncio.sleep(delay_between)

        print(f"Downloading {i+1}/{len(urls)}...", end=" ")
        result = await client.download(url, quality=Quality.P720)
        print(result.status.value)

See Also