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
Link File Format¶
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¶
- Discovery — automatic page crawling
- Playlists — playlist-specific workflows
- Python API — full method signatures