Initial commit

This commit is contained in:
Jarian Cottingham 2025-12-06 01:17:27 -06:00
commit cd4e2c56ba
16 changed files with 734 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
venv/
__pycache__/

117
README.md Normal file
View File

@ -0,0 +1,117 @@
# YouTube CLI
A command-line interface for browsing and downloading YouTube videos.
## Features
- Search YouTube videos with keyword queries
- Display up to 15 videos at a time with title, author, duration, and type (short/video)
- Download videos using yt-dlp with progress indication
- Configure download locations
- Handle short videos (videos with /shorts/ in URL) with special "(short)" prefix
## Requirements
- Python 3.6+
- yt-dlp
- rich
- requests
## Installation
### From Source
```bash
git clone https://github.com/yourusername/youtube-cli.git
cd youtube-cli
pip install -e .
```
### Using pip
```bash
pip install youtube-cli
```
## Usage
### Basic Search
```bash
youtube-cli "python tutorial"
```
### Download a Video
```bash
youtube-cli --download "https://www.youtube.com/watch?v=xyz123"
```
### View Help
```bash
youtube-cli --help
```
## Configuration
The application will create a default configuration file at `~/.config/youtube_cli/config.json` if one doesn't exist. You can customize:
```json
{
"download_dir": "~/Downloads/youtube",
"default_locations": [
"~/Downloads/youtube",
"~/Movies/youtube",
"/tmp/youtube"
],
"max_videos_per_page": 15,
"yt_dlp_args": {
"format": "bestvideo[height=1080]+bestaudio/bestvideo[height<=1080]+bestaudio",
"write_thumbnail": true,
"extractor_args": "youtube:player-client=default,-tv_simply"
}
}
```
## How to Use
1. Run a search query to find videos
2. Videos will be displayed with:
- Title (short videos marked with "(short)")
- Author
- Duration
- Type indicator
3. Choose an option:
- `n` for next page of results
- `q` to quit
- Enter a number to select and download a video
## Features
- **Search**: Search YouTube videos using keyword queries
- **Pagination**: View 15 videos at a time with option for more
- **Download**: Download videos directly with progress indication
- **Short Detection**: Automatically detects and marks short videos
- **Configuration**: Customizable download locations
- **Cross-platform**: Works on macOS and Linux
## Dependencies
This tool depends on `yt-dlp` which must be installed separately:
```bash
pip install yt-dlp
```
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Create a Pull Request
## License
MIT License

14
config.json Normal file
View File

@ -0,0 +1,14 @@
{
"download_dir": "~/Downloads/youtube",
"default_locations": [
"~/Downloads/youtube",
"~/Movies/youtube",
"/tmp/youtube"
],
"max_videos_per_page": 15,
"yt_dlp_args": {
"format": "bestvideo[height=1080]+bestaudio/bestvideo[height<=1080]+bestaudio",
"write_thumbnail": true,
"extractor_args": "youtube:player-client=default,-tv_simply"
}
}

18
prompt.md Normal file
View File

@ -0,0 +1,18 @@
The goal is to make a cli app that lets you traverse videos through youtube search and listed all the videos that are display for search. It should show at max 15 videos at a time and give options for asking for more. It should denote what videos are shorts (very short videos with /short in the url) with a name starting with (short). It should allow the user to search all of youtube with a query.
Display the Title, Name, Author, Video length of each youtube video on the list.
Give an option to get the next 15 videos in the list.
Give an option to download the video and show a status bar for the download.
Provide a configuration file that the user can set the default download location for all of this.
Let the user select from a set of default locations for where to download videos.
When downloading the video, it should leverage yt-dlp. A valid command in yt-dlp looks like
yt-dlp \
--format "bestvideo[height=1080]+bestaudio/bestvideo[height<=1080]+bestaudio" \
--download-archive "/mnt/centralstoragemedia/Youtube/Caseoh/caseoh-archive" \
--playlist-items 1:2 "https://www.youtube.com/@caseoh_/videos" \
-o "/mnt/centralstoragemedia/Youtube/Caseoh/%(title)s.%(ext)s" \
--write-thumbnail --extractor-args "youtube:player-client=default,-tv_simply"
Coding Language should be python and it should work on Mac and Linux.

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
yt-dlp
rich
requests

14
run.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
# Simple run script for YouTube CLI application
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Activate virtual environment if it exists
if [ -f "$SCRIPT_DIR/venv/bin/activate" ]; then
source "$SCRIPT_DIR/venv/bin/activate"
fi
# Run the youtube-cli application
python -m youtube_cli "$@"

33
setup.py Normal file
View File

@ -0,0 +1,33 @@
from setuptools import find_packages, setup
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="youtube-cli",
version="0.1.0",
author="Your Name",
author_email="your.email@example.com",
description="A command-line interface for browsing and downloading YouTube videos",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/yourusername/youtube-cli",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.6",
install_requires=[
"yt-dlp",
"rich",
"requests",
],
entry_points={
"console_scripts": [
"youtube-cli=youtube_cli.main:main",
],
},
include_package_data=True,
)

View File

@ -0,0 +1,142 @@
Metadata-Version: 2.4
Name: youtube-cli
Version: 0.1.0
Summary: A command-line interface for browsing and downloading YouTube videos
Home-page: https://github.com/yourusername/youtube-cli
Author: Your Name
Author-email: your.email@example.com
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Requires-Dist: yt-dlp
Requires-Dist: rich
Requires-Dist: requests
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary
# YouTube CLI
A command-line interface for browsing and downloading YouTube videos.
## Features
- Search YouTube videos with keyword queries
- Display up to 15 videos at a time with title, author, duration, and type (short/video)
- Download videos using yt-dlp with progress indication
- Configure download locations
- Handle short videos (videos with /shorts/ in URL) with special "(short)" prefix
## Requirements
- Python 3.6+
- yt-dlp
- rich
- requests
## Installation
### From Source
```bash
git clone https://github.com/yourusername/youtube-cli.git
cd youtube-cli
pip install -e .
```
### Using pip
```bash
pip install youtube-cli
```
## Usage
### Basic Search
```bash
youtube-cli "python tutorial"
```
### Download a Video
```bash
youtube-cli --download "https://www.youtube.com/watch?v=xyz123"
```
### View Help
```bash
youtube-cli --help
```
## Configuration
The application will create a default configuration file at `~/.config/youtube_cli/config.json` if one doesn't exist. You can customize:
```json
{
"download_dir": "~/Downloads/youtube",
"default_locations": [
"~/Downloads/youtube",
"~/Movies/youtube",
"/tmp/youtube"
],
"max_videos_per_page": 15,
"yt_dlp_args": {
"format": "bestvideo[height=1080]+bestaudio/bestvideo[height<=1080]+bestaudio",
"write_thumbnail": true,
"extractor_args": "youtube:player-client=default,-tv_simply"
}
}
```
## How to Use
1. Run a search query to find videos
2. Videos will be displayed with:
- Title (short videos marked with "(short)")
- Author
- Duration
- Type indicator
3. Choose an option:
- `n` for next page of results
- `q` to quit
- Enter a number to select and download a video
## Features
- **Search**: Search YouTube videos using keyword queries
- **Pagination**: View 15 videos at a time with option for more
- **Download**: Download videos directly with progress indication
- **Short Detection**: Automatically detects and marks short videos
- **Configuration**: Customizable download locations
- **Cross-platform**: Works on macOS and Linux
## Dependencies
This tool depends on `yt-dlp` which must be installed separately:
```bash
pip install yt-dlp
```
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Create a Pull Request
## License
MIT License

View File

@ -0,0 +1,11 @@
README.md
setup.py
youtube_cli/__init__.py
youtube_cli/__main__.py
youtube_cli/main.py
youtube_cli.egg-info/PKG-INFO
youtube_cli.egg-info/SOURCES.txt
youtube_cli.egg-info/dependency_links.txt
youtube_cli.egg-info/entry_points.txt
youtube_cli.egg-info/requires.txt
youtube_cli.egg-info/top_level.txt

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,2 @@
[console_scripts]
youtube-cli = youtube_cli.main:main

View File

@ -0,0 +1,3 @@
yt-dlp
rich
requests

View File

@ -0,0 +1 @@
youtube_cli

5
youtube_cli/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""
YouTube CLI - A command-line interface for browsing and downloading YouTube videos
"""
__version__ = "0.1.0"

9
youtube_cli/__main__.py Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env python3
"""
YouTube CLI - A command-line interface for browsing and downloading YouTube videos
"""
from .main import main
if __name__ == "__main__":
main()

359
youtube_cli/main.py Normal file
View File

@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""
YouTube CLI - A command-line interface for browsing and downloading YouTube videos
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
console = Console()
def main():
"""Main entry point for the YouTube CLI application."""
parser = argparse.ArgumentParser(
description="YouTube CLI - Browse and download YouTube videos"
)
parser.add_argument("query", nargs="?", help="Search query for YouTube")
parser.add_argument(
"--download", action="store_true", help="Download a specific video"
)
parser.add_argument("--config", help="Configuration file path")
parser.add_argument(
"--list", action="store_true", help="List videos (default behavior)"
)
parser.add_argument("--page", type=int, default=1, help="Page number for results")
args = parser.parse_args()
# Initialize configuration
config = load_config(args.config)
if args.download:
# Handle download functionality
if not args.query:
print("Error: You must provide a video URL for downloading")
return
download_video(args.query, config)
return
# Default behavior - show search results
if args.query:
search_videos(args.query, config, page=args.page)
else:
# Show help if no arguments given
parser.print_help()
def load_config(config_path=None):
"""Load configuration from file or use defaults."""
default_config = {
"download_dir": str(Path.home() / "Downloads" / "youtube"),
"default_locations": [
str(Path.home() / "Downloads" / "youtube"),
str(Path.home() / "Movies" / "youtube"),
"/tmp/youtube",
],
"max_videos_per_page": 15,
"yt_dlp_args": {
"format": "bestvideo[height=1080]+bestaudio/bestvideo[height<=1080]+bestaudio",
"write_thumbnail": True,
"extractor_args": "youtube:player-client=default,-tv_simply",
},
}
if config_path and os.path.exists(config_path):
try:
with open(config_path, "r") as f:
config = json.load(f)
# Merge with defaults
for key, value in default_config.items():
if key not in config:
config[key] = value
return config
except Exception as e:
print(f"Error loading config: {e}")
return default_config
def search_videos(query, config, page=1):
"""Search YouTube videos based on the query using yt-dlp."""
console.print(f"[blue]Searching YouTube for:[/blue] {query}")
# Construct yt-dlp command for search
cmd = [
"yt-dlp",
"--flat-playlist", # Get video info without downloading
"--dump-single-json", # Output as JSON for single video
f"--playlist-start={15 * (page - 1) + 1}",
f"--playlist-end={15 * page}",
"--no-warnings",
"--no-progress",
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
f"ytsearch{15 * page}:{query}",
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
console.print(f"[red]Error searching videos: {result.stderr}[/red]")
return
# Parse JSON output
import json
try:
data = json.loads(result.stdout.strip())
except json.JSONDecodeError:
console.print(
"[red]Error parsing search results. Try another search.[/red]"
)
return
# Process videos into our format
videos = []
if isinstance(data, list):
videos_data = data
elif "entries" in data:
videos_data = data["entries"]
else:
videos_data = [data]
for entry in videos_data:
if not entry:
continue
title = entry.get("title", "Unknown Title")
author = entry.get("uploader", "Unknown Author")
duration = entry.get("duration", 0)
url = entry.get("url", "") or entry.get("webpage_url", "")
# Format duration
length = format_duration(duration)
# Check if this is a short video
is_short = "/shorts/" in url
# Create video object
videos.append(
{
"title": title,
"author": author,
"length": length,
"url": url,
"is_short": is_short,
"id": entry.get("id", ""),
"thumbnail": entry.get("thumbnail", ""),
}
)
if not videos:
console.print("[yellow]No videos found for your search.[/yellow]")
return
display_videos(videos, config, page=page)
except subprocess.TimeoutExpired:
console.print("[red]Search timed out. Please try again.[/red]")
except Exception as e:
console.print(f"[red]Error during search: {str(e)}[/red]")
def format_duration(seconds):
"""Convert seconds to MM:SS or HH:MM:SS format."""
if not seconds:
return "0:00"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
if hours > 0:
return f"{hours}:{minutes:02d}:{secs:02d}"
else:
return f"{minutes}:{secs:02d}"
def display_videos(videos, config, page=1):
"""Display videos in a formatted table."""
console.print("\n" + "=" * 80)
console.print(f"[bold]YouTube Search Results - Page {page}[/bold]")
console.print("=" * 80)
table = Table(
title=f"Page {page} of search results",
show_header=True,
header_style="bold magenta",
)
table.add_column("No.", style="dim", width=3)
table.add_column("Title", width=35)
table.add_column("Author", width=20)
table.add_column("Duration", width=10)
table.add_column("Type", width=8)
for i, video in enumerate(videos, 1):
title = video["title"]
author = video["author"]
length = video["length"]
# Mark short videos
if video["is_short"]:
title = f"(short) {title}"
table.add_row(
str(i),
title,
author[:18] + "..." if len(author) > 18 else author,
length,
"Short" if video["is_short"] else "Video",
)
console.print(table)
# Show pagination options
console.print("=" * 80)
console.print("[blue]Options:[/blue]")
console.print(" [green]n[/green] - Next page")
console.print(" [red]q[/red] - Quit")
console.print(" [yellow]Number[/yellow] - Select and download video (e.g., 1)")
# Get user input
user_input = input("\nChoose an option: ").strip().lower()
if user_input == "q":
console.print("[green]Goodbye![/green]")
return
elif user_input == "n":
console.print(f"[blue]Loading page {page + 1}...[/blue]")
search_videos(
videos[0]["title"].split(" ")[0], config, page=page + 1
) # Simple approach
return
else:
try:
choice = int(user_input)
if 1 <= choice <= len(videos):
selected_video = videos[choice - 1]
console.print(
f"\n[blue]Selected video:[/blue] {selected_video['title']}"
)
# Download the video
download_video(selected_video["url"], config)
else:
console.print("[red]Invalid selection. Please try again.[/red]")
except ValueError:
console.print("[red]Invalid input. Please enter a number or 'q'/'n'.[/red]")
def download_video(url, config):
"""Download a video using yt-dlp with progress bar."""
console.print(f"[blue]Preparing to download:[/blue] {url}")
# Check if yt-dlp is available
try:
subprocess.run(["yt-dlp", "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
console.print(
"[red]Error: yt-dlp not found. Please install it with 'pip install yt-dlp'[/red]"
)
return
# Set download directory
download_dir = Path(config["download_dir"])
download_dir.mkdir(parents=True, exist_ok=True)
# Prepare yt-dlp command
cmd = [
"yt-dlp",
"--no-warnings",
"-o",
str(download_dir / "%(title)s.%(ext)s"),
"--write-thumbnail",
]
# Add custom args from config if they exist
ytdlp_args = config.get("yt_dlp_args", {})
if "format" in ytdlp_args:
cmd.extend(["--format", ytdlp_args["format"]])
if ytdlp_args.get("write_thumbnail", False):
cmd.append("--write-thumbnail")
# Add extractor args
if "extractor_args" in ytdlp_args:
cmd.extend(["--extractor-args", ytdlp_args["extractor_args"]])
# Add URL
cmd.append(url)
try:
console.print("[blue]Starting download...[/blue]")
# Run command with progress tracking
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
)
# Create a simple status indicator
progress_console = Console()
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=progress_console,
) as progress:
task = progress.add_task("[blue]Downloading...", total=100)
while True:
output = process.stdout.readline()
if output == "" and process.poll() is not None:
break
if output:
# Simple progress tracking (yt-dlp doesn't support progress events directly)
progress.update(task, advance=1)
# Check if download completed successfully
return_code = process.poll()
if return_code == 0:
console.print("[green]Download completed successfully![/green]")
else:
console.print("[red]Download failed.[/red]")
except Exception as e:
console.print(f"[red]Error during download: {str(e)}[/red]")
# Optionally, ask to select a different download location
locations = config.get("default_locations", [config["download_dir"]])
if len(locations) > 1:
console.print("\n[blue]Select download location:[/blue]")
for i, loc in enumerate(locations, 1):
console.print(f" {i}. {loc}")
try:
choice = int(input("Enter selection (or press Enter to use default): "))
if 1 <= choice <= len(locations):
new_dir = Path(locations[choice - 1])
config["download_dir"] = str(new_dir)
console.print(f"[green]Download location set to: {new_dir}[/green]")
except ValueError:
pass
if __name__ == "__main__":
main()