YouTube Commands
Overview
The netapi youtube commands provide CLI access to the YouTube Data API v3. All commands support -f json for jq piping.
Prerequisites
# Load API keys from dsec
dsource d000 dev/app
# Credentials available as environment variables:
# YOUTUBE_API_KEY - API key for read-only operations
# YOUTUBE_OAUTH_TOKEN - OAuth2 token for write operations (optional)
Create API key at: console.cloud.google.com/apis/credentials
Enable "YouTube Data API v3" under APIs & Services.
Add YOUTUBE_API_KEY=AIza… to dsec edit d000 dev/app.
Free tier: 10,000 quota units/day. Search costs 100 units, most list operations cost 1-5 units.
Commands (12 total)
| Command | Description |
|---|---|
|
Search videos, channels, playlists (cost: 100 units) |
|
Get video details (cost: 1 unit) |
|
Get channel details (cost: 1 unit) |
|
List playlist items (cost: 1 unit) |
|
Get video comment threads (cost: 1 unit) |
|
List available caption tracks (cost: 50 units) |
|
Trending videos by region (cost: 1 unit) |
|
List video categories (cost: 1 unit) |
|
Related videos for a given video (cost: 100 units) |
|
List authenticated user’s subscriptions (OAuth2, cost: 1 unit) |
|
Check daily quota usage |
|
Raw API request to any endpoint |
JSON Output
All commands support -f json for machine-readable output:
netapi youtube search "network automation" -f json
netapi youtube video dQw4w9WgXcQ -f json
netapi youtube channel UC_x5XG1OV2P6uZZ5FSM9Ttw -f json
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json
jq Patterns
Exploring Structure
# See all keys in first item
netapi youtube search "ansible" -f json | jq '.items[0] | keys'
# Keys with types
netapi youtube video dQw4w9WgXcQ -f json | jq '.items[0] | to_entries | .[] | "\(.key): \(.value | type)"'
# Full first object
netapi youtube search "netdevops" -f json | jq '.items[0]'
# Check page info and total results
netapi youtube search "python api" -f json | jq '.pageInfo'
Search Results
# Titles and channel names
netapi youtube search "network automation" -f json | jq -r '.items[] | "\(.snippet.title) — \(.snippet.channelTitle)"'
# Video IDs only (for piping)
netapi youtube search "ansible tutorial" -f json | jq -r '.items[] | select(.id.kind == "youtube#video") | .id.videoId'
# Search with structured output
netapi youtube search "python requests" -f json | jq '.items[] | {
title: .snippet.title,
channel: .snippet.channelTitle,
published: .snippet.publishedAt[0:10],
videoId: .id.videoId,
description: (.snippet.description[0:80] + "...")
}'
# Filter to videos only (exclude channels and playlists)
netapi youtube search "devops" -f json | jq '.items[] | select(.id.kind == "youtube#video")'
# Search results as clickable URLs
netapi youtube search "wireshark tutorial" -f json | jq -r '.items[] | select(.id.videoId) | "https://youtube.com/watch?v=\(.id.videoId) \(.snippet.title)"'
Video Details
# Full video summary
netapi youtube video dQw4w9WgXcQ -f json | jq '.items[0] | {
title: .snippet.title,
channel: .snippet.channelTitle,
published: .snippet.publishedAt[0:10],
views: .statistics.viewCount,
likes: .statistics.likeCount,
comments: .statistics.commentCount,
duration: .contentDetails.duration,
tags: .snippet.tags
}'
# Statistics only
netapi youtube video dQw4w9WgXcQ -f json | jq '.items[0].statistics'
# Thumbnail URLs (all resolutions)
netapi youtube video dQw4w9WgXcQ -f json | jq '.items[0].snippet.thumbnails | to_entries | .[] | "\(.key): \(.value.url)"'
# Highest resolution thumbnail
netapi youtube video dQw4w9WgXcQ -f json | jq -r '.items[0].snippet.thumbnails | to_entries | sort_by(-.value.width) | .[0].value.url'
# Parse ISO 8601 duration to human-readable
netapi youtube video dQw4w9WgXcQ -f json | jq -r '
.items[0].contentDetails.duration |
capture("PT((?P<h>[0-9]+)H)?((?P<m>[0-9]+)M)?((?P<s>[0-9]+)S)?") |
[if .h then "\(.h)h" else null end,
if .m then "\(.m)m" else null end,
if .s then "\(.s)s" else null end] |
map(select(. != null)) | join(" ")
'
# Multiple videos at once (comma-separated IDs)
netapi youtube video "dQw4w9WgXcQ,9bZkp7q19f0,kJQP7kiw5Fk" -f json | jq -r '.items[] | "\(.snippet.title): \(.statistics.viewCount) views"'
# Check if video has captions
netapi youtube video dQw4w9WgXcQ -f json | jq '.items[0].contentDetails.caption'
Channel Information
# Channel summary
netapi youtube channel UC_x5XG1OV2P6uZZ5FSM9Ttw -f json | jq '.items[0] | {
title: .snippet.title,
description: (.snippet.description[0:120] + "..."),
subscribers: .statistics.subscriberCount,
videos: .statistics.videoCount,
views: .statistics.viewCount,
created: .snippet.publishedAt[0:10],
country: .snippet.country
}'
# Channel branding links
netapi youtube channel UC_x5XG1OV2P6uZZ5FSM9Ttw -f json | jq '.items[0].brandingSettings.channel'
# Subscriber and video count (quick check)
netapi youtube channel UC_x5XG1OV2P6uZZ5FSM9Ttw -f json | jq -r '.items[0] | "\(.snippet.title): \(.statistics.subscriberCount) subs, \(.statistics.videoCount) videos"'
Playlist Items
# List all video titles in a playlist
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq -r '.items[] | "\(.snippet.position + 1). \(.snippet.title)"'
# Extract all video IDs (the killer use case)
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq -r '.items[].snippet.resourceId.videoId'
# Playlist items with metadata
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq '.items[] | {
position: (.snippet.position + 1),
title: .snippet.title,
videoId: .snippet.resourceId.videoId,
channel: .snippet.videoOwnerChannelTitle,
added: .snippet.publishedAt[0:10]
}'
# Playlist as URL list
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq -r '.items[] | "https://youtube.com/watch?v=\(.snippet.resourceId.videoId)"'
# Count items in playlist
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq '.pageInfo.totalResults'
Pipe Playlist to yt-dlp
This is the primary workflow for downloading playlist content without touching the YouTube frontend:
# Download entire playlist via yt-dlp
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json \
| jq -r '.items[].snippet.resourceId.videoId' \
| xargs -I{} yt-dlp "https://youtube.com/watch?v={}"
# Audio only (podcast playlists)
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json \
| jq -r '.items[].snippet.resourceId.videoId' \
| xargs -I{} yt-dlp -x --audio-format mp3 "https://youtube.com/watch?v={}"
# Parallel downloads (4 at a time)
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json \
| jq -r '.items[].snippet.resourceId.videoId' \
| xargs -P4 -I{} yt-dlp "https://youtube.com/watch?v={}"
# Download search results
netapi youtube search "conference talk kubernetes" -f json \
| jq -r '.items[] | select(.id.videoId) | .id.videoId' \
| xargs -I{} yt-dlp "https://youtube.com/watch?v={}"
Comments
# Top-level comments with like counts
netapi youtube comments dQw4w9WgXcQ -f json | jq '.items[] | {
author: .snippet.topLevelComment.snippet.authorDisplayName,
text: .snippet.topLevelComment.snippet.textDisplay,
likes: .snippet.topLevelComment.snippet.likeCount,
published: .snippet.topLevelComment.snippet.publishedAt[0:10]
}'
# Sort by likes (most popular comments)
netapi youtube comments dQw4w9WgXcQ -f json | jq '[.items[] | {
author: .snippet.topLevelComment.snippet.authorDisplayName,
text: .snippet.topLevelComment.snippet.textDisplay,
likes: .snippet.topLevelComment.snippet.likeCount
}] | sort_by(-.likes) | .[0:10]'
# Comment text only
netapi youtube comments dQw4w9WgXcQ -f json | jq -r '.items[].snippet.topLevelComment.snippet.textDisplay'
# Comments with reply counts
netapi youtube comments dQw4w9WgXcQ -f json | jq '.items[] | select(.snippet.totalReplyCount > 0) | {
text: .snippet.topLevelComment.snippet.textDisplay[0:80],
replies: .snippet.totalReplyCount
}'
Captions
# List available caption tracks
netapi youtube captions dQw4w9WgXcQ -f json | jq '.items[] | {
id: .id,
language: .snippet.language,
name: .snippet.name,
kind: .snippet.trackKind
}'
# Check if auto-generated captions exist
netapi youtube captions dQw4w9WgXcQ -f json | jq '.items[] | select(.snippet.trackKind == "ASR") | .snippet.language'
# Languages available
netapi youtube captions dQw4w9WgXcQ -f json | jq -r '.items[].snippet.language'
Trending Videos
# Trending in US (default)
netapi youtube trending -f json | jq -r '.items[] | "\(.snippet.title) — \(.statistics.viewCount) views"'
# Trending by region
netapi youtube trending --region GB -f json | jq '.items[] | {
title: .snippet.title,
channel: .snippet.channelTitle,
views: .statistics.viewCount,
category: .snippet.categoryId
}'
# Filter trending by category (10 = Music, 20 = Gaming, 28 = Science & Technology)
netapi youtube trending -f json | jq '.items[] | select(.snippet.categoryId == "28") | {
title: .snippet.title,
channel: .snippet.channelTitle,
views: .statistics.viewCount
}'
# Sort trending by view count
netapi youtube trending -f json | jq '[.items[] | {
title: .snippet.title,
views: (.statistics.viewCount | tonumber)
}] | sort_by(-.views) | .[] | "\(.views) — \(.title)"'
# Top 5 trending with URLs
netapi youtube trending -f json | jq -r '[.items[] | {
title: .snippet.title,
views: (.statistics.viewCount | tonumber),
id: .id
}] | sort_by(-.views) | .[0:5] | .[] | "https://youtube.com/watch?v=\(.id) \(.title)"'
Video Categories
# List all categories for a region
netapi youtube categories --region US -f json | jq -r '.items[] | "\(.id): \(.snippet.title)"'
# Assignable categories only
netapi youtube categories --region US -f json | jq -r '.items[] | select(.snippet.assignable) | "\(.id): \(.snippet.title)"'
Subscriptions (OAuth2)
# List subscriptions
netapi youtube subscriptions -f json | jq -r '.items[] | .snippet.title'
# Subscriptions with details
netapi youtube subscriptions -f json | jq '.items[] | {
channel: .snippet.title,
channelId: .snippet.resourceId.channelId,
description: (.snippet.description[0:80])
}'
# Count subscriptions
netapi youtube subscriptions -f json | jq '.pageInfo.totalResults'
Raw API Access
# Any GET endpoint (base: https://www.googleapis.com/youtube/v3)
netapi youtube api /videos?id=dQw4w9WgXcQ&part=snippet,statistics | jq '.'
# Channel sections
netapi youtube api /channelSections?channelId=UC_x5XG1OV2P6uZZ5FSM9Ttw&part=snippet | jq '.'
# Search with custom parameters
netapi youtube api "/search?q=linux&type=video&maxResults=5&order=viewCount&part=snippet" | jq '.items[].snippet.title'
# Video abuse report reasons (reference data)
netapi youtube api /videoAbuseReportReasons?part=snippet | jq '.'
# i18n regions
netapi youtube api /i18nRegions?part=snippet | jq '.items[] | {id: .id, name: .snippet.name}'
# Activities for a channel
netapi youtube api "/activities?channelId=UC_x5XG1OV2P6uZZ5FSM9Ttw&part=snippet&maxResults=10" | jq '.items[].snippet.title'
Environment Variables
| Variable | Description |
|---|---|
|
Google API key for read-only operations (required) |
|
OAuth2 access token for write operations and private data |
|
Default region code for trending and categories (default: US) |
Useful jq Recipes
Export to CSV
# Search results to CSV
netapi youtube search "network automation" -f json | jq -r '.items[] | select(.id.videoId) | [.snippet.title, .snippet.channelTitle, .id.videoId, .snippet.publishedAt[0:10]] | @csv'
# Video stats to CSV
netapi youtube video "id1,id2,id3" -f json | jq -r '["Title","Views","Likes","Comments"], (.items[] | [.snippet.title, .statistics.viewCount, .statistics.likeCount, .statistics.commentCount]) | @csv'
# Playlist to CSV
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq -r '.items[] | [(.snippet.position + 1), .snippet.title, .snippet.resourceId.videoId] | @csv'
Create Markdown Table
# Trending as markdown table
netapi youtube trending -f json | jq -r '["Title", "Channel", "Views"], ["---", "---", "---"], (.items[] | [.snippet.title, .snippet.channelTitle, .statistics.viewCount]) | @tsv' | column -t -s$'\t'
# Playlist as markdown table
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json | jq -r '["#", "Title", "Video ID"], ["---", "---", "---"], (.items[] | [(.snippet.position + 1), .snippet.title, .snippet.resourceId.videoId]) | @tsv' | column -t -s$'\t'
Count by Field
# Comments by author
netapi youtube comments dQw4w9WgXcQ -f json | jq '[.items[].snippet.topLevelComment.snippet.authorDisplayName] | group_by(.) | map({author: .[0], count: length}) | sort_by(-.count)'
# Trending by category ID
netapi youtube trending -f json | jq '[.items[].snippet.categoryId] | group_by(.) | map({category: .[0], count: length}) | sort_by(-.count)'
Filter and Transform
# Videos over 1M views from search
netapi youtube search "conference talk" -f json | jq -r '
[.items[] | select(.id.videoId)] | map(.id.videoId) | join(",")
' | xargs -I{} netapi youtube video "{}" -f json | jq '
.items[] | select((.statistics.viewCount | tonumber) > 1000000) | {
title: .snippet.title,
views: .statistics.viewCount,
url: "https://youtube.com/watch?v=\(.id)"
}
'
# Videos published in the last 30 days from a channel's uploads
netapi youtube channel UCxxxxxxxxxxxxxxxxxxxxxx -f json | jq -r '.items[0].contentDetails.relatedPlaylists.uploads' \
| xargs -I{} netapi youtube playlist "{}" -f json \
| jq --arg d "$(date -d '30 days ago' -Iseconds)" '.items[] | select(.snippet.publishedAt > $d) | .snippet.title'
# Duration filter: find videos longer than 20 minutes
netapi youtube playlist PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -f json \
| jq -r '.items[].snippet.resourceId.videoId' | paste -sd, \
| xargs -I{} netapi youtube video "{}" -f json \
| jq '.items[] | select(
.contentDetails.duration | test("[0-9]H") or
(capture("PT((?P<m>[0-9]+)M)?") | (.m // "0") | tonumber > 20)
) | {title: .snippet.title, duration: .contentDetails.duration}'
curl Equivalents (Works Now)
These raw curl + jq patterns work today without the netapi CLI. Requires YOUTUBE_API_KEY environment variable.
Setup
-
Get your API key: console.cloud.google.com/apis/credentials (enable YouTube Data API v3 first at console.cloud.google.com/apis/library/youtube.googleapis.com)
-
Add to dsec:
dsec edit d000 dev/appAdd this line to the
app.env.agefile:YOUTUBE_API_KEY=AIza...
-
Load credentials:
dsource d000 dev/app
Search Videos
# Search videos
curl -s "https://www.googleapis.com/youtube/v3/search?part=snippet&q=python+asyncio+tutorial&type=video&maxResults=10&key=${YOUTUBE_API_KEY}" \
| jq '.items[] | {title: .snippet.title, channel: .snippet.channelTitle, videoId: .id.videoId}'
# Search with URL output
curl -s "https://www.googleapis.com/youtube/v3/search?part=snippet&q=cisco+ise&type=video&maxResults=5&key=${YOUTUBE_API_KEY}" \
| jq -r '.items[] | "[\(.snippet.title)] https://youtube.com/watch?v=\(.id.videoId)"'
Video Details
# Video metadata + statistics
curl -s "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id=dQw4w9WgXcQ&key=${YOUTUBE_API_KEY}" \
| jq '.items[0] | {
title: .snippet.title,
channel: .snippet.channelTitle,
published: .snippet.publishedAt[0:10],
duration: .contentDetails.duration,
views: .statistics.viewCount,
likes: .statistics.likeCount,
comments: .statistics.commentCount
}'
# Multiple videos at once (comma-separated IDs)
curl -s "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&id=dQw4w9WgXcQ,jNQXAC9IVRw&key=${YOUTUBE_API_KEY}" \
| jq '.items[] | {title: .snippet.title, views: .statistics.viewCount}'
Channel Info
# Channel by ID
curl -s "https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id=UC_x5XG1OV2P6uZZ5FSM9Ttw&key=${YOUTUBE_API_KEY}" \
| jq '.items[0] | {
name: .snippet.title,
subscribers: .statistics.subscriberCount,
videos: .statistics.videoCount,
views: .statistics.viewCount,
description: .snippet.description[0:200]
}'
# Channel by username
curl -s "https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&forHandle=@NetworkChuck&key=${YOUTUBE_API_KEY}" \
| jq '.items[0] | {name: .snippet.title, subscribers: .statistics.subscriberCount}'
Playlist Items
# List playlist contents
curl -s "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=PLxxxxxx&maxResults=50&key=${YOUTUBE_API_KEY}" \
| jq '.items[] | {title: .snippet.title, videoId: .snippet.resourceId.videoId, position: .snippet.position}'
# Extract video IDs for yt-dlp
curl -s "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=PLxxxxxx&maxResults=50&key=${YOUTUBE_API_KEY}" \
| jq -r '.items[].snippet.resourceId.videoId' \
| xargs -I{} echo "https://youtube.com/watch?v={}"
# Pipe directly to yt-dlp (audio only)
curl -s "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=PLxxxxxx&maxResults=50&key=${YOUTUBE_API_KEY}" \
| jq -r '.items[].snippet.resourceId.videoId' \
| xargs -P4 -I{} yt-dlp -x --audio-format mp3 "https://youtube.com/watch?v={}"
Comments
# Video comments (top-level, sorted by relevance)
curl -s "https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId=dQw4w9WgXcQ&maxResults=20&order=relevance&key=${YOUTUBE_API_KEY}" \
| jq '.items[] | {
author: .snippet.topLevelComment.snippet.authorDisplayName,
text: .snippet.topLevelComment.snippet.textDisplay | gsub("<[^>]+>"; ""),
likes: .snippet.topLevelComment.snippet.likeCount,
replies: .snippet.totalReplyCount
}'
Trending Videos
# Trending in US
curl -s "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&chart=mostPopular®ionCode=US&maxResults=10&key=${YOUTUBE_API_KEY}" \
| jq '.items[] | {title: .snippet.title, channel: .snippet.channelTitle, views: .statistics.viewCount}'
# Trending by category (28 = Science & Technology)
curl -s "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&chart=mostPopular®ionCode=US&videoCategoryId=28&maxResults=10&key=${YOUTUBE_API_KEY}" \
| jq '.items[] | {title: .snippet.title, views: .statistics.viewCount}'