Initial commit
This commit is contained in:
commit
cd4e2c56ba
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
117
README.md
Normal file
117
README.md
Normal 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
14
config.json
Normal 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
18
prompt.md
Normal 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
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
yt-dlp
|
||||||
|
rich
|
||||||
|
requests
|
||||||
14
run.sh
Executable file
14
run.sh
Executable 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
33
setup.py
Normal 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,
|
||||||
|
)
|
||||||
142
youtube_cli.egg-info/PKG-INFO
Normal file
142
youtube_cli.egg-info/PKG-INFO
Normal 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
|
||||||
11
youtube_cli.egg-info/SOURCES.txt
Normal file
11
youtube_cli.egg-info/SOURCES.txt
Normal 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
|
||||||
1
youtube_cli.egg-info/dependency_links.txt
Normal file
1
youtube_cli.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
youtube_cli.egg-info/entry_points.txt
Normal file
2
youtube_cli.egg-info/entry_points.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
youtube-cli = youtube_cli.main:main
|
||||||
3
youtube_cli.egg-info/requires.txt
Normal file
3
youtube_cli.egg-info/requires.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
yt-dlp
|
||||||
|
rich
|
||||||
|
requests
|
||||||
1
youtube_cli.egg-info/top_level.txt
Normal file
1
youtube_cli.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
youtube_cli
|
||||||
5
youtube_cli/__init__.py
Normal file
5
youtube_cli/__init__.py
Normal 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
9
youtube_cli/__main__.py
Normal 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
359
youtube_cli/main.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user