How to Build a CLI YouTube Viewer in PythonWatching YouTube from the terminal can be fast, distraction-free, and scriptable. This guide walks you through building a simple, extensible Command-Line Interface (CLI) YouTube viewer in Python that can search for videos, stream them in a terminal media player, and optionally download them. The project emphasizes modular design, good UX in the terminal, and respectful use of YouTube (follow Terms of Service).
What this project will do
- Search YouTube for videos by query.
- Display results in a clear, navigable list in the terminal.
- Stream selected videos using mpv (recommended) or youtube-dl/yt-dlp + mpv.
- Optionally download videos or audio.
- Handle basic errors (network, invalid input) gracefully.
- Be structured so you can add features later (playlists, authentication, caching).
Prerequisites
- Python 3.8+
- A terminal on Linux/macOS/Windows (WSL recommended on Windows)
- mpv (for streaming)
- yt-dlp (preferred over youtube-dl for reliability)
- Optional: spotdl or other tools for audio extraction
Install tools on Debian/Ubuntu:
sudo apt update sudo apt install -y mpv python3-pip pip install yt-dlp
Install on macOS with Homebrew:
brew install mpv pip3 install yt-dlp
Install on Windows:
- Install mpv from mpv.io or scoop/chocolatey.
- Install Python and pip, then:
pip install yt-dlp
Python libraries we’ll use:
- requests (for calling YouTube Search API or scraping)
- rich (for pretty terminal output and selection)
- subprocess (builtin) to call mpv/yt-dlp
- optionally, google-api-python-client if using YouTube Data API
Install Python packages:
pip install requests rich
Choosing how to search YouTube
Two main approaches:
- Use the YouTube Data API (official, reliable, requires API key and quota).
- Scrape YouTube search results or use a third-party service (no API key, may break).
This guide will show both: a simple scraping approach for ease of use, and an optional YouTube Data API method for stability.
Project structure
- cli_youtube_viewer/
- main.py
- search.py
- player.py
- downloader.py
- utils.py
- requirements.txt
- README.md
Core components
search.py — getting search results
We’ll implement a lightweight scraper using YouTube’s search HTML (works but may break). For reliable usage, use YouTube Data API (example included).
search.py (scraper version):
import requests import re import json from typing import List, Dict YOUTUBE_SEARCH_URL = "https://www.youtube.com/results" def search_youtube(query: str, max_results: int = 10) -> List[Dict]: params = {"search_query": query, "pbj": 1} headers = {"User-Agent": "Mozilla/5.0"} resp = requests.get(YOUTUBE_SEARCH_URL, params={"search_query": query}, headers=headers, timeout=10) html = resp.text # Extract initial data JSON m = re.search(r"var ytInitialData = ({.*?});", html, re.S) if not m: # alternative: look for "ytInitialData =" m = re.search(r"ytInitialData = ({.*?});", html, re.S) if not m: raise RuntimeError("Could not extract ytInitialData") data = json.loads(m.group(1)) items = [] # Walk the JSON to find videoRenderer items def walk(node): if isinstance(node, dict): if "videoRenderer" in node: items.append(node["videoRenderer"]) for v in node.values(): walk(v) elif isinstance(node, list): for el in node: walk(el) walk(data) results = [] for vid in items[:max_results]: video_id = vid.get("videoId") title_runs = vid.get("title", {}).get("runs", []) title = "".join([r.get("text","") for r in title_runs]) length = vid.get("lengthText", {}).get("simpleText", "") channel = vid.get("ownerText", {}).get("runs", [{}])[0].get("text","") results.append({"id": video_id, "title": title, "length": length, "channel": channel}) return results
search.py (YouTube Data API version):
from googleapiclient.discovery import build from typing import List, Dict def search_youtube_api(api_key: str, query: str, max_results: int = 10) -> List[Dict]: youtube = build("youtube", "v3", developerKey=api_key) res = youtube.search().list(q=query, part="snippet", maxResults=max_results, type="video").execute() results = [] for item in res.get("items", []): results.append({ "id": item["id"]["videoId"], "title": item["snippet"]["title"], "channel": item["snippet"]["channelTitle"], "publishedAt": item["snippet"]["publishedAt"] }) return results
player.py — streaming with mpv
We’ll stream using mpv called as a subprocess. This avoids downloading and supports many formats.
player.py:
import subprocess from typing import Optional def play_with_mpv(video_url: str, start: Optional[int] = None): cmd = ["mpv", "--no-terminal", video_url] if start: cmd.extend(["--start", str(start)]) subprocess.run(cmd)
Use yt-dlp to resolve playable URL if needed:
import subprocess, json def resolve_url(video_id: str) -> str: cmd = ["yt-dlp", "-J", f"https://youtube.com/watch?v={video_id}"] out = subprocess.check_output(cmd) info = json.loads(out) # prefer best video+audio (or direct url) formats = info.get("formats", []) # find best mpv-playable format (e.g. mp4/webm) formats_sorted = sorted(formats, key=lambda f: f.get("height") or 0, reverse=True) for f in formats_sorted: if f.get("url"): return f["url"] raise RuntimeError("No playable url found")
downloader.py — optional downloads
Use yt-dlp to download a video or extract audio.
downloader.py:
import subprocess def download_video(video_id: str, output_template: str = "%(title)s.%(ext)s"): cmd = ["yt-dlp", "-o", output_template, f"https://youtube.com/watch?v={video_id}"] subprocess.run(cmd) def download_audio(video_id: str, output_template: str = "%(title)s.%(ext)s"): cmd = ["yt-dlp", "-x", "--audio-format", "mp3", "-o", output_template, f"https://youtube.com/watch?v={video_id}"] subprocess.run(cmd)
main.py — CLI UX
Use rich for nice output and Prompt for selection.
main.py:
import sys from rich.console import Console from rich.table import Table from search import search_youtube from player import resolve_url, play_with_mpv from downloader import download_video, download_audio console = Console() def main(): if len(sys.argv) < 2: console.print("Usage: python main.py <search query>") sys.exit(1) query = " ".join(sys.argv[1:]) results = search_youtube(query, max_results=10) table = Table(show_header=True, header_style="bold magenta") table.add_column("Index", style="dim", width=6) table.add_column("Title") table.add_column("Channel", style="dim") table.add_column("Length", justify="right") for i, r in enumerate(results, start=1): table.add_row(str(i), r["title"], r.get("channel",""), r.get("length","")) console.print(table) choice = console.input("Select a video index to play (or 'd' to download, 'q' to quit): ") if choice.lower() == 'q': return if choice.lower() == 'd': idx = int(console.input("Index to download: ")) vid = results[idx-1]["id"] download_video(vid) return idx = int(choice) vid = results[idx-1]["id"] url = resolve_url(vid) play_with_mpv(url) if __name__ == "__main__": main()
Error handling & improvements
- Add try/except around network calls and subprocess calls; show friendly messages.
- Cache search results locally to reduce repeated scraping/API calls.
- Add pagination to fetch more results.
- Respect YouTube Terms of Service; avoid automated mass downloading.
- Add keyboard navigation (arrow keys) using python-prompt-toolkit or textual.
- Add configuration file (~/.config/cli-youtube-viewer/config.json) for default player, download path, API key.
Packaging & distribution
- requirements.txt with dependencies.
- Add entry point in setup.cfg/pyproject.toml for pip installable package.
- Create a simple shell wrapper script for easy invocation: cli-youtube-viewer “query”.
requirements.txt example:
requests rich yt-dlp google-api-python-client # optional
Example usage
-
Search and play:
- python main.py “lofi hip hop”
- select index 1 to play in mpv
-
Download audio:
- run and choose ’d’, then enter index.
Testing
- Unit-test search parsing with saved HTML fixtures.
- Mock subprocess calls for player and downloader functions.
- Integration test with YouTube Data API (if using API key) in CI with rate limits.
Security and legal notes
- This is for personal use and learning. Large-scale scraping or downloading may violate YouTube’s Terms of Service. Use the official API for production, and obey copyright.
If you want, I can:
- Provide the full project as a single zipped script.
- Convert this to use prompt-toolkit for arrow-key navigation.
- Swap scraping for only YouTube Data API code and show how to set an API key.
Leave a Reply