Skip to content

Planning

Planning is the process of classifying raw user input (URLs, file paths, lists) and building a download plan without executing downloads.

What is Planning?

Planning answers: "What would be downloaded if we execute this?"

By default, planning is cheap and local: - No network calls (except for inspecting the input type) - Fast response - Returns an AcquisitionPlan with all items

This is useful for: - Previewing what will be downloaded - Building UIs that let users select specific items - Automating batch workflows

plan() — Local Classification

plan() classifies input locally without remote inspection:

CLI

Not directly exposed on CLI, but used internally by batch operations.

Python

import asyncio
from vdl import VDLClient

async def main():
    client = VDLClient()

    # Single URL
    plan = await client.plan("https://example.com/video")
    print(f"Items: {plan.total}")
    print(f"Kind: {plan.kind}")

    # Playlist URL
    plan = await client.plan("https://example.com/playlist")
    print(f"Items: {plan.total}")

    # File with URLs
    plan = await client.plan("~/links.txt")
    print(f"Items: {plan.total}")

    # Comma-separated URLs
    plan = await client.plan("https://example.com/v1, https://example.com/v2")
    print(f"Items: {plan.total}")

asyncio.run(main())

plan_with_probe() — Remote Inspection

plan_with_probe() calls inspect_url for network-based classification, useful for ambiguous URLs:

# Ambiguous: is this a single video or a playlist member?
plan = await client.plan_with_probe("https://example.com/watch?v=...")
# Resolves intent and classifies correctly

Input Classification

VDL recognizes these input types:

Input Type Plan Kind Action
Direct video URL VIDEO_URL DIRECT_MEDIA Download as single item
Playlist URL PLAYLIST_URL PLAYLIST Create item per playlist entry
Ambiguous video URL VIDEO_URL (ambiguous) AMBIGUOUS_MEMBER Marked is_ambiguous=True in metadata
Local .txt file LOCAL_LINK_FILE MULTI_URL Parse URLs from file, one per line
Multiple URLs URL MULTI_URL Split by comma or space
Discovery target (URL) DISCOVERY_CANDIDATES Results from page crawl
Unsupported UNKNOWN UNSUPPORTED Empty plan

AcquisitionPlan

The result of planning:

@dataclass(frozen=True)
class AcquisitionPlan:
    items: list[AcquisitionItem]    # What to download
    source_kind: InputKind          # Original input type
    metadata: dict[str, Any]        # Context: is_ambiguous, is_supported, etc.

    @property
    def total(self) -> int:         # Number of items
    @property
    def kind(self) -> PlanKind:     # Classified plan type

AcquisitionItem

Each downloadable item:

@dataclass(frozen=True)
class AcquisitionItem:
    source_url: str                 # URL to download
    output_dir: Path               # Where to save
    quality: Quality               # Quality preference
    kind: DownloadKind             # VIDEO, AUDIO, or PLAYLIST
    if_exists: IfExists            # Collision policy
    subdirectory: str | None       # Optional subfolder
    is_playlist: bool              # Is this a playlist URL?
    metadata: dict[str, Any]       # Extra context

A simple text file, one URL per line:

# Comment lines start with #
https://example.com/video1
https://example.com/video2
https://example.com/playlist

# Blank lines are ignored
https://example.com/video3

Supported in both CLI and Python API.

Nested Playlist Expansion

By default, playlists are treated as single items. Enable expansion:

vdl config set expand_nested_playlists true

Then a link file with a playlist URL will expand into individual items:

# links.txt
https://example.com/playlist   # 5 items
https://example.com/video1

Expands to 6 items total.

Capped by max_playlist_items:

vdl config set expand_nested_playlists true
vdl config set max_playlist_items 20

Plan Classification

Use plan_requires_choice() to check if user interaction is needed:

plan = await client.plan(user_input)

choice_type = client.plan_requires_choice(plan)
# Returns: "none", "ambiguous_playlist", "playlist_selection", "discovered_selection"

if choice_type == "playlist_selection":
    # Show user a list of items to select from
    pass
elif choice_type == "ambiguous_playlist":
    # Ask user: download this video or its playlist?
    pass

Executing a Plan

Once you have a plan, execute it:

plan = await client.plan("~/links.txt")

# Execute all items
results = await client.execute_plan(plan)

# Or with selection
selection = [1, 3, 5]  # Item indexes
results = await client.execute_plan(plan, selection=selection)

Plan Metadata

Plans include metadata for context:

plan = await client.plan("URL")

# Check if input is supported
if not plan.metadata.get("is_supported"):
    print("URL not recognized")

# Check if it's ambiguous
if plan.metadata.get("is_ambiguous"):
    print("Could be video or playlist member")

# Discovery metadata
if "discovery" in plan.metadata:
    report = plan.metadata["discovery"]
    print(f"Matched rule: {report.matched_rule}")
    print(f"Total links found: {report.total_links_seen}")

PlanKind Values

Kind Meaning
DIRECT_MEDIA Single video/audio
PLAYLIST Playlist, treated as one item
MULTI_URL Multiple separate URLs
AMBIGUOUS_MEMBER Could be video or playlist member
DISCOVERY_CANDIDATES Results from page crawl
UNSUPPORTED Cannot determine or unsupported input

Access via: plan.kind (property)

Workflow Diagram

flowchart TD
    Input["user_input<br/>(URL, file, list)"]
    Classify["analyze_input()"]

    Video["VIDEO_URL<br/>→ DIRECT_MEDIA"]
    Playlist["PLAYLIST_URL<br/>→ PLAYLIST"]
    File["LOCAL_LINK_FILE<br/>→ MULTI_URL"]
    Ambiguous["VIDEO_URL ambiguous<br/>→ AMBIGUOUS_MEMBER"]
    Unknown["UNKNOWN<br/>→ UNSUPPORTED"]

    Plan["AcquisitionPlan"]

    UserChoice{"plan_requires_choice?"}

    Execute["execute_plan()"]
    Download["DownloadExecutionService"]
    Result["list[DownloadResult]"]

    Input --> Classify
    Classify --> Video
    Classify --> Playlist
    Classify --> File
    Classify --> Ambiguous
    Classify --> Unknown

    Video --> Plan
    Playlist --> Plan
    File --> Plan
    Ambiguous --> Plan
    Unknown --> Plan

    Plan --> UserChoice
    UserChoice -- no --> Execute
    UserChoice -- yes --> Execute
    Execute --> Download
    Download --> Result

Examples

See demos/download_link_file.py for a complete example of plan() and execute_plan().

See Also