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

Search videos, channels, playlists (cost: 100 units)

video

Get video details (cost: 1 unit)

channel

Get channel details (cost: 1 unit)

playlist

List playlist items (cost: 1 unit)

comments

Get video comment threads (cost: 1 unit)

captions

List available caption tracks (cost: 50 units)

trending

Trending videos by region (cost: 1 unit)

categories

List video categories (cost: 1 unit)

related

Related videos for a given video (cost: 100 units)

subscriptions

List authenticated user’s subscriptions (OAuth2, cost: 1 unit)

quota

Check daily quota usage

api

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 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'

Quota Usage

# Check remaining quota
netapi youtube quota -f json | jq '{
  used: .quotaUsed,
  limit: .quotaLimit,
  remaining: (.quotaLimit - .quotaUsed),
  reset: .resetTime
}'

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

YOUTUBE_API_KEY

Google API key for read-only operations (required)

YOUTUBE_OAUTH_TOKEN

OAuth2 access token for write operations and private data

YOUTUBE_REGION

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

  1. 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)

  2. Add to dsec:

    dsec edit d000 dev/app

    Add this line to the app.env.age file:

    YOUTUBE_API_KEY=AIza...
  3. 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 in US
curl -s "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&chart=mostPopular&regionCode=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&regionCode=US&videoCategoryId=28&maxResults=10&key=${YOUTUBE_API_KEY}" \
  | jq '.items[] | {title: .snippet.title, views: .statistics.viewCount}'

Quota Check

# YouTube doesn't have a direct quota endpoint.
# Check usage at: https://console.cloud.google.com/apis/api/youtube.googleapis.com/quotas
# Costs: search = 100 units, videos.list = 1 unit, channels.list = 1 unit
# Daily limit: 10,000 units (free tier)