Compare commits
11 Commits
bbfd646f8b
...
c58943f6b8
| Author | SHA1 | Date | |
|---|---|---|---|
| c58943f6b8 | |||
| 3c5b10466a | |||
| 483611f357 | |||
| 7842cb1f7f | |||
| 6ae8f3befc | |||
| 7432ad644c | |||
| 3adafe8bbd | |||
| 4538857ce3 | |||
| 2789b03f45 | |||
| b33bed7a6e | |||
| 669b677fb3 |
38
.gitea/workflows/publish.yml
Normal file
38
.gitea/workflows/publish.yml
Normal file
@ -0,0 +1,38 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITEA_URL: https://git.example.com
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repo
|
||||
run: |
|
||||
rm -rf $GITHUB_WORKSPACE/*
|
||||
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: twine upload dist/*
|
||||
29
.github/workflows/test.yml
vendored
Normal file
29
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --locked
|
||||
|
||||
- name: Lint
|
||||
run: uv run ruff check .
|
||||
|
||||
- name: Run tests
|
||||
run: uv run python -m pytest tests/ -v
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 jarianc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,187 +0,0 @@
|
||||
# Comprehensive Logging Implementation - Final Summary
|
||||
|
||||
## Project: YouTube CLI
|
||||
## Date: February 25, 2026
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
### 1. Logs Directory Structure
|
||||
```
|
||||
/Users/user/Projects/youtube-cli/logs/
|
||||
├── app.log # Main application log file (auto-created)
|
||||
└── .gitignore (logs/ already present)
|
||||
```
|
||||
|
||||
### 2. Logging Configuration
|
||||
|
||||
**Global Settings:**
|
||||
- **Log Directory**: `~/.config/youtube_cli/logs/`
|
||||
- **Log File**: `app.log`
|
||||
- **Max Size**: 10MB
|
||||
- **Backup Count**: 5 rotations
|
||||
- **File Level**: DEBUG
|
||||
- **Console Level**: INFO
|
||||
|
||||
**Log Format:**
|
||||
```
|
||||
YYYY-MM-DD HH:MM:SS | module.name | LEVEL | message
|
||||
```
|
||||
|
||||
### 3. Files Modified
|
||||
|
||||
| File | Logger Added | Console Print Replaced |
|
||||
|------|-------------|----------------------|
|
||||
| `youtube_cli/main.py` | ✅ | 30+ statements |
|
||||
| `youtube_tui/app.py` | ✅ | N/A (no console.print) |
|
||||
| `youtube_tui/services/download_manager.py` | ✅ | 3 statements |
|
||||
| `youtube_tui/services/youtube.py` | ✅ | 4 statements |
|
||||
| `youtube_tui/services/queue.py` | ✅ | 2 statements |
|
||||
| `app.py` (REST API) | ✅ | N/A (no console.print) |
|
||||
|
||||
### 4. Logging Implementation Pattern
|
||||
|
||||
**Standard Import Block:**
|
||||
```python
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
# Configure logging
|
||||
LOG_DIR = Path.home() / ".config" / "youtube_cli" / "logs"
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOG_FILE = LOG_DIR / "app.log"
|
||||
|
||||
file_handler = RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
"%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
))
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
handlers=[file_handler, console_handler],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
**Usage Pattern:**
|
||||
```python
|
||||
logger.debug("Detailed debugging info")
|
||||
logger.info("Normal operations")
|
||||
logger.warning("Non-critical issues")
|
||||
logger.error("Errors with full context")
|
||||
logger.exception("Exceptions with traceback")
|
||||
```
|
||||
|
||||
### 5. Logging Coverage
|
||||
|
||||
#### youtube_cli/main.py
|
||||
- **Search Operations**: Query logging, pagination, result display
|
||||
- **Download Operations**: URL validation, directory setup, format selection, completion status
|
||||
- **Archive Operations**: Loading, saving, adding videos
|
||||
- **Configuration**: Loading, validation, defaults
|
||||
- **Network Share**: Copy operations, path validation
|
||||
- **Error Handling**: Full error context with file paths and parameters
|
||||
|
||||
#### youtube_tui/services/download_manager.py
|
||||
- **Download Management**: Queue processing, cancellation, errors
|
||||
- **Progress Tracking**: Download status updates
|
||||
- **Error Recovery**: Exception handling with context
|
||||
|
||||
#### youtube_tui/services/youtube.py
|
||||
- **Service Errors**: Search failures, download errors, archive issues
|
||||
- **Async Operations**: Thread execution tracking
|
||||
|
||||
#### youtube_tui/services/queue.py
|
||||
- **Queue Operations**: Loading, saving, status updates
|
||||
- **Item Management**: Add/remove operations
|
||||
|
||||
#### app.py (REST API)
|
||||
- **API Operations**: Request handling, error responses
|
||||
- **Module Initialization**: Flask app setup
|
||||
|
||||
### 6. Benefits
|
||||
|
||||
1. **Production Monitoring**: Full visibility into application behavior
|
||||
2. **Troubleshooting**: Detailed logs with timestamps and module context
|
||||
3. **Disk Space Management**: Automatic log rotation prevents unbounded growth
|
||||
4. **Development Efficiency**: Console output for quick debugging, file logs for detailed analysis
|
||||
5. **Consistency**: Unified logging approach across all modules
|
||||
|
||||
### 7. Testing Results
|
||||
|
||||
**Verification Commands:**
|
||||
```bash
|
||||
# Test logging
|
||||
python3 -c "from youtube_cli.main import logger; logger.info('Test message')"
|
||||
|
||||
# View logs
|
||||
tail -f ~/.config/youtube_cli/logs/app.log
|
||||
|
||||
# Search logs
|
||||
grep "ERROR" ~/.config/youtube_cli/logs/app.log
|
||||
grep "WARNING" ~/.config/youtube_cli/logs/app.log
|
||||
```
|
||||
|
||||
**Test Output:**
|
||||
```
|
||||
2026-02-25 20:14:40 | youtube_cli.main | INFO | Test info message
|
||||
2026-02-25 20:14:40 | youtube_cli.main | WARNING | Test warning message
|
||||
2026-02-25 20:14:40 | youtube_cli.main | ERROR | Test error message
|
||||
```
|
||||
|
||||
### 8. Git Ignore
|
||||
|
||||
The `logs/` directory is already in `.gitignore`:
|
||||
```
|
||||
logs/
|
||||
```
|
||||
|
||||
This ensures log files are never committed to the repository.
|
||||
|
||||
### 9. Next Steps for Users
|
||||
|
||||
1. **View logs**: `tail -f ~/.config/youtube_cli/logs/app.log`
|
||||
2. **Search errors**: `grep "ERROR" ~/.config/youtube_cli/logs/app.log`
|
||||
3. **Debug mode**: Check DEBUG level messages in the log file
|
||||
4. **Rotation management**: Logs automatically rotate at 10MB with 5 backups
|
||||
|
||||
### 10. Files Created/Modified
|
||||
|
||||
**Created:**
|
||||
- `/Users/user/Projects/youtube-cli/logs/` (directory)
|
||||
- `/Users/user/Projects/youtube-cli/LOGGING_SUMMARY.md` (documentation)
|
||||
|
||||
**Modified:**
|
||||
- `/Users/user/Projects/youtube-cli/youtube_cli/main.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/app.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/services/download_manager.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/services/youtube.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/services/queue.py`
|
||||
- `/Users/user/Projects/youtube-cli/app.py`
|
||||
|
||||
### 11. Requirements Met
|
||||
|
||||
✅ Logs directory created at `/Users/user/Projects/youtube-cli/logs/`
|
||||
✅ Logs directory added to `.gitignore`
|
||||
✅ Logging configured with rotation (10MB max, 5 backups)
|
||||
✅ Logging added to all specified files:
|
||||
- `/Users/user/Projects/youtube-cli/youtube_cli/main.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/app.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/services/download_manager.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/services/youtube.py`
|
||||
- `/Users/user/Projects/youtube-cli/youtube_tui/services/queue.py`
|
||||
- `/Users/user/Projects/youtube-cli/app.py`
|
||||
✅ Module-level loggers created
|
||||
✅ All console.print statements replaced with appropriate logger calls
|
||||
✅ Logging initialized in main entry points
|
||||
✅ Summary document provided
|
||||
|
||||
---
|
||||
|
||||
**Implementation Complete**: All requirements have been met and verified.
|
||||
@ -1,114 +0,0 @@
|
||||
# Logging Implementation Summary
|
||||
|
||||
## Overview
|
||||
Comprehensive logging has been added to all Python files in the YouTube CLI project.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Logs Directory
|
||||
- Created `/Users/user/Projects/youtube-cli/logs/` directory
|
||||
- Added `logs/` to `.gitignore` (already present)
|
||||
|
||||
### 2. Logging Configuration
|
||||
All Python files now have:
|
||||
- **Log file**: `~/.config/youtube_cli/logs/app.log`
|
||||
- **Rotation**: 10MB max, keep 5 backups
|
||||
- **Level**: DEBUG (file), INFO (console)
|
||||
- **Format**: `%(asctime)s | %(name)s | %(levelname)s | %(message)s`
|
||||
|
||||
### 3. Files Modified
|
||||
|
||||
#### `/Users/user/Projects/youtube-cli/youtube_cli/main.py`
|
||||
- Added `logging` and `RotatingFileHandler` imports
|
||||
- Added module-level logger: `logger = logging.getLogger(__name__)`
|
||||
- Replaced all `console.print()` statements with appropriate logger calls:
|
||||
- `logger.debug()` for verbose debugging info
|
||||
- `logger.info()` for normal operations
|
||||
- `logger.warning()` for warnings
|
||||
- `logger.error()` for errors
|
||||
- `logger.exception()` for exceptions (includes traceback)
|
||||
|
||||
Key logging additions:
|
||||
- Search operations
|
||||
- Download operations
|
||||
- Archive operations
|
||||
- Configuration loading
|
||||
- Network share operations
|
||||
- Error handling with full context
|
||||
|
||||
#### `/Users/user/Projects/youtube-cli/youtube_tui/app.py`
|
||||
- Added logging imports and configuration
|
||||
- Added module-level logger
|
||||
- Added logging initialization in `main()` function
|
||||
|
||||
#### `/Users/user/Projects/youtube-cli/youtube_tui/services/download_manager.py`
|
||||
- Added logging imports and configuration
|
||||
- Added module-level logger
|
||||
- Replaced `console.print()` with:
|
||||
- `logger.warning()` for cancellations
|
||||
- `logger.error()` for errors
|
||||
|
||||
#### `/Users/user/Projects/youtube-cli/youtube_tui/services/youtube.py`
|
||||
- Added logging imports and configuration
|
||||
- Added module-level logger
|
||||
- Replaced `self.console.print()` with `logger.error()` for service errors
|
||||
|
||||
#### `/Users/user/Projects/youtube-cli/youtube_tui/services/queue.py`
|
||||
- Added logging imports and configuration
|
||||
- Added module-level logger
|
||||
- Replaced `console.print()` with `logger.warning()` for queue operations
|
||||
|
||||
#### `/Users/user/Projects/youtube-cli/app.py` (REST API)
|
||||
- Added logging imports and configuration
|
||||
- Added module-level logger
|
||||
- Added logging initialization in module scope
|
||||
|
||||
### 4. Logging Levels Used
|
||||
|
||||
| Level | Usage | Example |
|
||||
|-------|-------|---------|
|
||||
| `DEBUG` | Detailed debugging info | Download directory paths |
|
||||
| `INFO` | Normal operations | Search queries, download starts, completions |
|
||||
| `WARNING` | Non-critical issues | Invalid inputs, skipped operations |
|
||||
| `ERROR` | Errors | Failed downloads, missing dependencies |
|
||||
| `CRITICAL` | Critical failures | Not used (reserved for severe issues) |
|
||||
|
||||
### 5. Benefits
|
||||
|
||||
1. **Production Monitoring**: Full visibility into application behavior
|
||||
2. **Troubleshooting**: Detailed logs with timestamps and context
|
||||
3. **Performance Tracking**: Log rotation prevents disk space issues
|
||||
4. **Debugging**: Both file and console output for development
|
||||
5. **Consistency**: Same logging approach across all modules
|
||||
|
||||
## Usage
|
||||
|
||||
### View Logs in Real-Time
|
||||
```bash
|
||||
tail -f ~/.config/youtube_cli/logs/app.log
|
||||
```
|
||||
|
||||
### Search Logs
|
||||
```bash
|
||||
grep "ERROR" ~/.config/youtube_cli/logs/app.log
|
||||
grep "WARNING" ~/.config/youtube_cli/logs/app.log
|
||||
```
|
||||
|
||||
### Log File Location
|
||||
- Logs are stored in: `~/.config/youtube_cli/logs/app.log`
|
||||
- Old logs are automatically rotated and archived
|
||||
|
||||
## Testing
|
||||
|
||||
To verify logging works:
|
||||
1. Run the CLI: `python -m youtube_cli "test query"`
|
||||
2. Run the TUI: `python youtube_tui/app.py`
|
||||
3. Run the API: `python app.py`
|
||||
4. Check logs: `cat ~/.config/youtube_cli/logs/app.log`
|
||||
|
||||
## Notes
|
||||
|
||||
- The `RotatingFileHandler` ensures log files don't grow indefinitely
|
||||
- Console output is limited to INFO level for cleaner terminal output
|
||||
- File output captures all DEBUG level messages for thorough logging
|
||||
- All exceptions are logged with full tracebacks using `logger.error()`
|
||||
10
MANIFEST.in
Normal file
10
MANIFEST.in
Normal file
@ -0,0 +1,10 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
include pyproject.toml
|
||||
include requirements.txt
|
||||
recursive-include youtube_cli *.py
|
||||
recursive-include youtube_tui *.py
|
||||
recursive-include tests *.py
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[cod]
|
||||
global-exclude .venv
|
||||
@ -350,4 +350,4 @@ youtube-tui
|
||||
|
||||
### Documentation
|
||||
|
||||
See [TUI.md](TUI.md) for complete TUI documentation.
|
||||
See [TUI.md](docs/TUI.md) for complete TUI documentation.
|
||||
122
agent.md
122
agent.md
@ -1,122 +0,0 @@
|
||||
# YouTube CLI Agent
|
||||
|
||||
## Project Overview
|
||||
This is a command-line interface for browsing and downloading YouTube videos. The application allows users to search YouTube, display videos in pages of 15, and download videos using yt-dlp with progress indication.
|
||||
|
||||
## Core Features
|
||||
- **Search YouTube videos** with keyword queries
|
||||
- **Display videos** with title, author, duration, and type (short/video)
|
||||
- **Download videos** using yt-dlp with progress indication
|
||||
- **Configure download locations** through configuration files
|
||||
- **Handle short videos** (videos with /shorts/ in URL) with special "(short)" prefix
|
||||
- **Pagination support** to view more than 15 videos per search
|
||||
- **Network share integration** for copying downloaded videos
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
youtube-cli/
|
||||
├── youtube_cli/ # Main package directory
|
||||
│ ├── __init__.py # Package initialization
|
||||
│ ├── __main__.py # Main entry point
|
||||
│ └── main.py # Core application logic
|
||||
├── setup.py # Python package setup
|
||||
├── requirements.txt # Python dependencies
|
||||
├── README.md # Project documentation
|
||||
├── prompt.md # Prompt template
|
||||
├── structure.md # Project structure documentation
|
||||
└── run.sh # Run script
|
||||
```
|
||||
|
||||
## Main Components
|
||||
|
||||
### youtube_cli/main.py
|
||||
The core application logic file containing:
|
||||
- `YouTubeCLI` class with all main functionality
|
||||
- Search videos using yt-dlp with pagination support
|
||||
- Display videos in formatted tables with Rich library
|
||||
- Download videos with progress indication
|
||||
- Configuration management
|
||||
- Archive tracking for downloaded videos
|
||||
- Network share copying functionality
|
||||
- Update checking for yt-dlp
|
||||
|
||||
### Key Methods in YouTubeCLI Class
|
||||
- `search_videos()`: Search YouTube videos using yt-dlp
|
||||
- `display_videos()`: Display videos in a formatted table
|
||||
- `download_video()`: Download individual videos with progress
|
||||
- `download_playlist()`: Download YouTube playlists
|
||||
- `load_config()`: Load configuration from file or use defaults
|
||||
- `load_archive()`: Load archive of already downloaded videos
|
||||
- `add_to_archive()`: Add videos to download archive
|
||||
- `copy_to_network_share()`: Copy downloaded videos to network share
|
||||
- `check_for_updates()`: Check and update yt-dlp if needed
|
||||
|
||||
## Configuration
|
||||
The application creates a default configuration file at `~/.config/youtube_cli/config.json` if one doesn't exist. Configuration includes:
|
||||
- `download_dir`: Default download directory (set to `/Volumes/MediaServer/Youtube/`)
|
||||
- `default_locations`: List of default download locations including Tech, AI, Art, Homes, Cooking, Fitness, Music, Gaming, Education, Travel, Business, Science, History, Comedy, News, Sports, Nature, Photography, Language, and Automotive categories
|
||||
- `max_videos_per_page`: Number of videos per page (default: 15)
|
||||
- `yt_dlp_args`: Custom yt-dlp arguments for video format and extraction
|
||||
- `network_share_path`: Network share path for copying videos
|
||||
- `default_network_subfolder`: Default subfolder on network share
|
||||
|
||||
## Usage Examples
|
||||
```bash
|
||||
# Search for videos
|
||||
youtube-cli "python tutorial"
|
||||
|
||||
# Download a specific video
|
||||
youtube-cli --download "https://www.youtube.com/watch?v=xyz123"
|
||||
|
||||
# View help
|
||||
youtube-cli --help
|
||||
|
||||
# Check for yt-dlp updates
|
||||
youtube-cli --check-update
|
||||
|
||||
# Update yt-dlp
|
||||
youtube-cli --update
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
- Python 3.6+
|
||||
- yt-dlp (for video downloading)
|
||||
- rich (for formatted console output)
|
||||
- requests (for API calls)
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## How It Works
|
||||
1. **Search**: Users enter a search query to find YouTube videos
|
||||
2. **Display**: Videos are shown 15 at a time with title, author, duration, and type
|
||||
3. **Navigation**: Users can go to next page (n), search for new term (s), or quit (q)
|
||||
4. **Selection**: Users can select videos by number to download
|
||||
5. **Download**: Videos are downloaded using yt-dlp with progress indication
|
||||
6. **Tracking**: Downloaded videos are tracked in an archive file
|
||||
|
||||
## Special Features
|
||||
- **Short Video Detection**: Automatically detects and marks short videos with "(short)" prefix
|
||||
- **Pagination**: View 15 videos at a time with option for more
|
||||
- **Multiple Download Locations**: Choose from default locations or specify custom paths
|
||||
- **Network Share Integration**: Copy downloaded videos to network shares after download
|
||||
- **Update Checking**: Automatically checks for and updates yt-dlp when needed
|
||||
- **Archive Tracking**: Keeps track of already downloaded videos to avoid duplicates
|
||||
|
||||
## Technical Details
|
||||
- Uses yt-dlp for all YouTube operations
|
||||
- Implements Rich library for beautiful console output
|
||||
- Supports both individual video and playlist downloads
|
||||
- Handles JavaScript challenges with remote components
|
||||
- Includes retry mechanisms for downloads
|
||||
- Cross-platform compatibility (macOS and Linux)
|
||||
14
app.log
14
app.log
@ -1,14 +0,0 @@
|
||||
Error saving archive: [Errno 13] Permission denied: '/app'
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: on
|
||||
[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:4096
|
||||
* Running on http://192.168.122.203:4096
|
||||
[33mPress CTRL+C to quit[0m
|
||||
* Restarting with stat
|
||||
Error saving archive: [Errno 13] Permission denied: '/app'
|
||||
* Debugger is active!
|
||||
* Debugger PIN: 556-686-523
|
||||
192.168.122.1 - - [03/Feb/2026 19:10:11] "GET /search?q=roo+vs+cline HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [03/Feb/2026 19:10:13] "GET /health HTTP/1.1" 200 -
|
||||
@ -4,8 +4,12 @@ Manual test script for YouTube TUI
|
||||
Tests all required functionality
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def print_section(title):
|
||||
@ -145,7 +149,7 @@ def test_unit_tests():
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
cwd="/Users/user/Projects/youtube-cli",
|
||||
cwd=str(REPO_ROOT),
|
||||
)
|
||||
|
||||
print(result.stdout)
|
||||
@ -431,12 +435,13 @@ print("OK")
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(REPO_ROOT)
|
||||
"""Run all tests"""
|
||||
print("=" * 60)
|
||||
print(" YouTube TUI Manual Test Suite")
|
||||
print("=" * 60)
|
||||
print(f"\nPython: {sys.version}")
|
||||
print("Working directory: /Users/user/Projects/youtube-cli")
|
||||
print(f"Working directory: {REPO_ROOT}")
|
||||
|
||||
results = []
|
||||
|
||||
|
||||
18
prompt.md
18
prompt.md
@ -1,18 +0,0 @@
|
||||
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.
|
||||
@ -9,7 +9,7 @@ description = "A command-line interface for browsing and downloading YouTube vid
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{name = "Your Name", email = "your.email@example.com"},
|
||||
{name = "Jarian Cottingham", email = "jarianc@proton.me"},
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
classifiers = [
|
||||
@ -43,8 +43,10 @@ youtube-cli = "youtube_cli.main:main"
|
||||
youtube-tui = "youtube_tui.__main__:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/youtube-cli"
|
||||
Issues = "https://github.com/yourusername/youtube-cli/issues"
|
||||
Homepage = "https://git.example.com/jarianc/youtube-cli"
|
||||
Issues = "https://git.example.com/jarianc/youtube-cli/issues"
|
||||
Documentation = "https://git.example.com/jarianc/youtube-cli"
|
||||
Source = "https://git.example.com/jarianc/youtube-cli"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
@ -60,7 +62,7 @@ api = [
|
||||
"Flask==2.3.3",
|
||||
]
|
||||
tui = [
|
||||
"textual>=0.40.0",
|
||||
"textual>=0.40.0,<2.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
17
setup.py
17
setup.py
@ -6,12 +6,18 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
||||
setup(
|
||||
name="youtube-cli",
|
||||
version="0.1.0",
|
||||
author="Your Name",
|
||||
author_email="your.email@example.com",
|
||||
author="Jarian Cottingham",
|
||||
author_email="jarianc@proton.me",
|
||||
description="A command-line interface for browsing and downloading YouTube videos",
|
||||
keywords="youtube, cli, download, video, yt-dlp, tui, terminal",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/yourusername/youtube-cli",
|
||||
url="https://git.example.com/jarianc/youtube-cli",
|
||||
project_urls={
|
||||
"Documentation": "https://git.example.com/jarianc/youtube-cli",
|
||||
"Issue Tracker": "https://git.example.com/jarianc/youtube-cli/issues",
|
||||
"Source": "https://git.example.com/jarianc/youtube-cli",
|
||||
},
|
||||
packages=find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
@ -26,16 +32,13 @@ setup(
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Utilities",
|
||||
],
|
||||
python_requires=">=3.7",
|
||||
python_requires=">=3.10",
|
||||
install_requires=[
|
||||
"yt-dlp",
|
||||
"rich>=13.0.0",
|
||||
|
||||
@ -40,15 +40,17 @@ class TestSearchScreen:
|
||||
|
||||
async def test_search_action_with_empty_input(self, app):
|
||||
"""Test search action with empty input"""
|
||||
from youtube_tui.screens.search import SearchScreen
|
||||
|
||||
screen = SearchScreen()
|
||||
# Use app.run_test() to ensure proper screen mounting
|
||||
async with app.run_test() as pilot:
|
||||
app.push_screen(screen)
|
||||
# Wait for the screen to be fully mounted
|
||||
await pilot.pause()
|
||||
|
||||
# Mock the update_status method to track calls
|
||||
update_calls = []
|
||||
screen = app.screen
|
||||
screen.update_status = lambda message: update_calls.append(message)
|
||||
|
||||
# Simulate pressing Enter with empty input
|
||||
@ -62,8 +64,11 @@ class TestSearchScreen:
|
||||
"""Test search action with valid input"""
|
||||
|
||||
search_term = "python tutorial"
|
||||
from youtube_tui.screens.search import SearchScreen
|
||||
|
||||
screen = SearchScreen()
|
||||
async with app.run_test() as pilot:
|
||||
app.push_screen(screen)
|
||||
# Wait for the screen to be fully mounted
|
||||
await pilot.pause()
|
||||
|
||||
@ -83,8 +88,11 @@ class TestSearchScreen:
|
||||
"""Test search from anywhere action"""
|
||||
|
||||
search_term = "music"
|
||||
from youtube_tui.screens.search import SearchScreen
|
||||
|
||||
screen = SearchScreen()
|
||||
async with app.run_test() as pilot:
|
||||
app.push_screen(screen)
|
||||
# Wait for the screen to be fully mounted
|
||||
await pilot.pause()
|
||||
|
||||
|
||||
@ -190,7 +190,7 @@ class TestCustomWidgets:
|
||||
from textual.widgets import Static
|
||||
|
||||
widget = Static("Test content")
|
||||
assert widget._Static__content == "Test content"
|
||||
assert widget.renderable == "Test content"
|
||||
|
||||
def test_static_widget_with_rich_markup(self):
|
||||
"""Test Static widget with Rich markup"""
|
||||
@ -198,14 +198,14 @@ class TestCustomWidgets:
|
||||
|
||||
widget = Static("[bold]Test[/bold] [red]content[/red]")
|
||||
# The content should contain the markup
|
||||
assert "[bold]" in str(widget._Static__content)
|
||||
assert "[bold]" in str(widget.renderable)
|
||||
|
||||
def test_button_widget_creation(self):
|
||||
"""Test Button widget"""
|
||||
from textual.widgets import Button
|
||||
|
||||
button = Button("Click me")
|
||||
assert button.label == "Click me"
|
||||
assert str(button.label) == "Click me"
|
||||
|
||||
def test_input_widget_creation(self):
|
||||
"""Test Input widget"""
|
||||
|
||||
17
uv.lock
generated
17
uv.lock
generated
@ -309,7 +309,7 @@ name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
@ -484,6 +484,9 @@ wheels = [
|
||||
linkify = [
|
||||
{ name = "linkify-it-py" },
|
||||
]
|
||||
plugins = [
|
||||
{ name = "mdit-py-plugins" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
@ -831,19 +834,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "textual"
|
||||
version = "8.0.2"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||
{ name = "mdit-py-plugins" },
|
||||
{ name = "markdown-it-py", extra = ["linkify", "plugins"] },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/08/c6bcb1e3c4c9528ec9049f4ac685afdafc72866664270f0deb416ccbba2a/textual-8.0.2.tar.gz", hash = "sha256:7b342f3ee9a5f2f1bd42d7b598cae00ff1275da68536769510db4b7fe8cabf5d", size = 6099270, upload-time = "2026-03-03T20:23:46.858Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/b6/59b1de04bb4dca0f21ed7ba0b19309ed7f3f5de4396edf20cc2855e53085/textual-1.0.0.tar.gz", hash = "sha256:bec9fe63547c1c552569d1b75d309038b7d456c03f86dfa3706ddb099b151399", size = 1532733, upload-time = "2024-12-12T10:42:03.286Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bc/0cd17f96f00b6e8bfbca64c574088c85f3c614912b3030f313752e30a099/textual-8.0.2-py3-none-any.whl", hash = "sha256:4ceadbe0e8a30eb80f9995000f4d031f711420a31b02da38f3482957b7c50ce4", size = 719174, upload-time = "2026-03-03T20:23:50.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/bb/5fb6656c625019cd653d5215237d7cd6e0b12e7eae4195c3d1c91b2136fc/textual-1.0.0-py3-none-any.whl", hash = "sha256:2d4a701781c05104925e463ae370c630567c70c2880e92ab838052e3e23c986f", size = 660456, upload-time = "2024-12-12T10:42:00.375Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -989,7 +990,7 @@ requires-dist = [
|
||||
{ name = "requests", specifier = ">=2.28.0" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
||||
{ name = "textual", marker = "extra == 'tui'", specifier = ">=0.40.0" },
|
||||
{ name = "textual", marker = "extra == 'tui'", specifier = ">=0.40.0,<2.0.0" },
|
||||
{ name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.28.0" },
|
||||
{ name = "yt-dlp" },
|
||||
]
|
||||
|
||||
@ -24,8 +24,9 @@ if os.environ.get('DOCKER'):
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
else:
|
||||
accesslog = "/home/user/repos/youtube-cli/web/server/gunicorn-access.log"
|
||||
errorlog = "/home/user/repos/youtube-cli/web/server/gunicorn-error.log"
|
||||
log_dir = os.environ.get("GUNICORN_LOG_DIR", os.path.dirname(os.path.abspath(__file__)))
|
||||
accesslog = os.path.join(log_dir, "gunicorn-access.log")
|
||||
errorlog = os.path.join(log_dir, "gunicorn-error.log")
|
||||
loglevel = "debug"
|
||||
|
||||
# Process naming
|
||||
|
||||
@ -754,7 +754,7 @@ class YouTubeCLI:
|
||||
"""Download a video using yt-dlp with progress bar."""
|
||||
|
||||
# Validate URL before proceeding
|
||||
if not url or not isinstance(url, str) or url.strip() == "":
|
||||
if not url or not isinstance(url, str):
|
||||
logger.error("Invalid or empty video URL provided.")
|
||||
return False
|
||||
|
||||
@ -825,9 +825,7 @@ class YouTubeCLI:
|
||||
else:
|
||||
logger.info("Using 1080p quality by default")
|
||||
|
||||
# Run command and let yt-dlp handle progress natively
|
||||
# Removed timeout to support long-running downloads in queue
|
||||
# Use a pipe to capture progress and support cancellation
|
||||
# Run command with progress bar
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
@ -836,12 +834,71 @@ class YouTubeCLI:
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Rich progress bar for downloads
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
DownloadColumn,
|
||||
Progress,
|
||||
TextColumn,
|
||||
TimeRemainingColumn,
|
||||
TransferSpeedColumn,
|
||||
)
|
||||
|
||||
progress = Progress(
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
BarColumn(bar_width=40),
|
||||
"[progress.percentage]{task.percentage:>3.1f}%",
|
||||
"•",
|
||||
DownloadColumn(),
|
||||
"•",
|
||||
TransferSpeedColumn(),
|
||||
"•",
|
||||
TimeRemainingColumn(),
|
||||
)
|
||||
|
||||
downloaded_bytes = 0
|
||||
total_bytes = 0
|
||||
download_task = None
|
||||
output_lines = []
|
||||
|
||||
try:
|
||||
stdout, _ = process.communicate(timeout=None) # No timeout
|
||||
with progress:
|
||||
for line in process.stdout:
|
||||
output_lines.append(line)
|
||||
|
||||
# Parse progress line: "[download] 5.0% of ~ 10.00MiB at 1.23MiB/s ETA 00:08"
|
||||
import re as re_mod
|
||||
|
||||
progress_match = re_mod.search(
|
||||
r"\[download\]\s+(\d+\.?\d*)%\s+of\s+[~]?\s*([\d.]+)\s*(B|KiB|MiB|GiB)",
|
||||
line,
|
||||
)
|
||||
if progress_match:
|
||||
pct = float(progress_match.group(1))
|
||||
size_str = progress_match.group(2)
|
||||
size_unit = progress_match.group(3)
|
||||
total_bytes = self._parse_size(size_str, size_unit)
|
||||
downloaded_bytes = int(total_bytes * pct / 100)
|
||||
|
||||
if download_task is None:
|
||||
download_task = progress.add_task(
|
||||
"Downloading...",
|
||||
total=total_bytes if total_bytes > 0 else None,
|
||||
)
|
||||
progress.update(
|
||||
download_task,
|
||||
completed=downloaded_bytes,
|
||||
total=total_bytes if total_bytes > 0 else None,
|
||||
)
|
||||
elif "[download]" in line and "[" not in line.split("[download]")[1].split()[0]:
|
||||
pass
|
||||
elif "[download] Deprecated commandline" in line:
|
||||
pass
|
||||
|
||||
result = subprocess.CompletedProcess(
|
||||
cmd, process.returncode, stdout, ""
|
||||
cmd, process.returncode, "".join(output_lines), ""
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
except KeyboardInterrupt:
|
||||
process.kill()
|
||||
logger.warning("Download cancelled by user")
|
||||
return False
|
||||
@ -855,7 +912,6 @@ class YouTubeCLI:
|
||||
|
||||
# Add video to archive for tracking
|
||||
try:
|
||||
# Extract video ID from URL for archive
|
||||
import re
|
||||
|
||||
video_id = None
|
||||
@ -880,8 +936,6 @@ class YouTubeCLI:
|
||||
if result.stdout:
|
||||
logger.error(f"Error details: {result.stdout}")
|
||||
|
||||
# Try to check if we have a different problem
|
||||
# Check for specific JavaScript challenge errors and recommend solutions
|
||||
if (
|
||||
"Solving JS challenges" in result.stdout
|
||||
or "challenge solving failed" in result.stdout
|
||||
@ -895,6 +949,11 @@ class YouTubeCLI:
|
||||
except Exception as e:
|
||||
logger.error(f"Error during download: {str(e)}")
|
||||
|
||||
def _parse_size(self, size_str: str, unit: str) -> int:
|
||||
"""Parse size string with unit to bytes."""
|
||||
multipliers = {"B": 1, "KiB": 1024, "MiB": 1024**2, "GiB": 1024**3}
|
||||
return int(float(size_str) * multipliers.get(unit, 1))
|
||||
|
||||
def download_playlist(
|
||||
self,
|
||||
url,
|
||||
|
||||
@ -131,7 +131,7 @@ class YouTubeTUI(App):
|
||||
)
|
||||
self.download_manager.start_processing()
|
||||
|
||||
self.push_search_screen()
|
||||
self.push_home_screen()
|
||||
self.current_screen = self.screen
|
||||
|
||||
def on_screen_stack_changed(self) -> None:
|
||||
@ -141,6 +141,12 @@ class YouTubeTUI(App):
|
||||
self.current_search_term = current_screen.search_term
|
||||
self.current_screen = current_screen
|
||||
|
||||
def push_home_screen(self) -> None:
|
||||
"""Push the home screen"""
|
||||
from youtube_tui.screens.home import HomeScreen
|
||||
|
||||
self.push_screen(HomeScreen())
|
||||
|
||||
def push_search_screen(self) -> None:
|
||||
"""Push the search screen"""
|
||||
from youtube_tui.screens.search import SearchScreen
|
||||
|
||||
@ -121,9 +121,16 @@ class DownloadScreen(Screen):
|
||||
async def start_download(self) -> None:
|
||||
"""Start the download process"""
|
||||
try:
|
||||
# Perform the download (async method)
|
||||
async def progress_callback(percentage: int) -> bool:
|
||||
"""Update progress bar during download"""
|
||||
self.update_progress(percentage)
|
||||
self.update_status(
|
||||
f"[blue]Downloading... {percentage}%[/blue]"
|
||||
)
|
||||
return True
|
||||
|
||||
success = await self.youtube_service.download_video(
|
||||
self.video, self.category
|
||||
self.video, self.category, progress_callback=progress_callback
|
||||
)
|
||||
|
||||
if success:
|
||||
|
||||
192
youtube_tui/screens/home.py
Normal file
192
youtube_tui/screens/home.py
Normal file
@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Home Screen for YouTube TUI
|
||||
Main dashboard with quick actions
|
||||
"""
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
Footer,
|
||||
Header,
|
||||
Static,
|
||||
)
|
||||
|
||||
|
||||
class HomeScreen(Screen):
|
||||
"""Main dashboard screen with quick actions"""
|
||||
|
||||
CSS = """
|
||||
HomeScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#welcome-container {
|
||||
width: 60%;
|
||||
height: auto;
|
||||
border: double #555555;
|
||||
padding: 2 3;
|
||||
margin: 2 0;
|
||||
}
|
||||
|
||||
#welcome-title {
|
||||
width: 100%;
|
||||
height: 3;
|
||||
content-align: center middle;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#welcome-info {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 2;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#quick-actions {
|
||||
width: 100%;
|
||||
layout: grid;
|
||||
grid-gutter: 1;
|
||||
grid-columns: 2;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 100%;
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
#status-info {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 2;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#status-bar {
|
||||
dock: bottom;
|
||||
height: 1;
|
||||
background: $surface;
|
||||
color: $text-muted;
|
||||
padding: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("s", "open_search", "Search"),
|
||||
("q", "open_queue", "Queue"),
|
||||
("h", "open_history", "History"),
|
||||
("ctrl+h", "show_help", "Help"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.download_queue = None
|
||||
self.download_manager = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the home screen"""
|
||||
yield Header()
|
||||
yield Container(
|
||||
Static(
|
||||
"[bold cyan]YouTube CLI[/bold cyan] - Browse and download videos",
|
||||
id="welcome-title",
|
||||
),
|
||||
Static(
|
||||
"Use keyboard shortcuts or buttons to navigate",
|
||||
id="welcome-info",
|
||||
),
|
||||
Container(
|
||||
Button("Search Videos", id="search-btn"),
|
||||
Button("Download Queue", id="queue-btn"),
|
||||
Button("Search History", id="history-btn"),
|
||||
Button("Help", id="help-btn"),
|
||||
id="quick-actions",
|
||||
),
|
||||
Static(id="status-info"),
|
||||
id="welcome-container",
|
||||
)
|
||||
yield Static(id="status-bar")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when screen is mounted"""
|
||||
if hasattr(self.app, "download_queue"):
|
||||
self.download_queue = self.app.download_queue
|
||||
if hasattr(self.app, "download_manager"):
|
||||
self.download_manager = self.app.download_manager
|
||||
|
||||
self.update_queue_status()
|
||||
self.update_status("[green]Welcome to YouTube TUI - Press 's' to search[/green]")
|
||||
|
||||
def update_queue_status(self) -> None:
|
||||
"""Update queue status info"""
|
||||
if self.download_manager:
|
||||
status = self.download_manager.get_queue_status()
|
||||
status_text = (
|
||||
f"Queue: {status['pending_count']} pending, "
|
||||
f"{status['downloading_count']} downloading, "
|
||||
f"{status['total_count']} total"
|
||||
)
|
||||
else:
|
||||
status_text = "Queue: No active downloads"
|
||||
|
||||
status_info = self.query_one("#status-info", Static)
|
||||
status_info.update(f"[dim]{status_text}[/dim]")
|
||||
|
||||
def update_status(self, message: str) -> None:
|
||||
"""Update the status bar message"""
|
||||
status_bar = self.query_one("#status-bar", Static)
|
||||
status_bar.update(f"[bold white]{message}[/bold white]")
|
||||
|
||||
def action_open_search(self) -> None:
|
||||
"""Open search screen"""
|
||||
if hasattr(self.app, "push_search_screen"):
|
||||
self.app.push_search_screen()
|
||||
else:
|
||||
from youtube_tui.screens.search import SearchScreen
|
||||
|
||||
self.app.push_screen(SearchScreen())
|
||||
|
||||
def action_open_queue(self) -> None:
|
||||
"""Open queue screen"""
|
||||
if hasattr(self.app, "action_open_queue"):
|
||||
self.app.action_open_queue()
|
||||
else:
|
||||
from youtube_tui.screens.queue import QueueScreen
|
||||
|
||||
self.app.push_screen(QueueScreen())
|
||||
|
||||
def action_open_history(self) -> None:
|
||||
"""Open search history"""
|
||||
from youtube_tui.screens.history import SearchHistoryScreen
|
||||
|
||||
self.app.push_screen(SearchHistoryScreen())
|
||||
|
||||
def action_show_help(self) -> None:
|
||||
"""Show help screen"""
|
||||
from youtube_tui.screens.help import HelpScreen
|
||||
|
||||
self.app.push_screen(HelpScreen())
|
||||
|
||||
def action_quit(self) -> None:
|
||||
"""Quit the application"""
|
||||
self.app.exit()
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Handle escape key"""
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses"""
|
||||
if event.button.id == "search-btn":
|
||||
self.action_open_search()
|
||||
elif event.button.id == "queue-btn":
|
||||
self.action_open_queue()
|
||||
elif event.button.id == "history-btn":
|
||||
self.action_open_history()
|
||||
elif event.button.id == "help-btn":
|
||||
self.action_show_help()
|
||||
@ -130,6 +130,21 @@ class QueueScreen(Screen):
|
||||
self.update_stats()
|
||||
self.update_status("[green]Queue loaded[/green]")
|
||||
|
||||
# Start auto-refresh if downloads active
|
||||
if self.download_manager and self.download_manager.is_processing():
|
||||
self.set_interval(2, self._auto_refresh)
|
||||
|
||||
def _auto_refresh(self) -> None:
|
||||
"""Auto-refresh queue table every 2 seconds"""
|
||||
self.update_table()
|
||||
self.update_stats()
|
||||
|
||||
# Stop auto-refresh when no downloads active
|
||||
if self.download_manager:
|
||||
status = self.download_manager.get_queue_status()
|
||||
if not status.get("has_active_download") and not status.get("pending_count"):
|
||||
self.clear_interval(self._auto_refresh)
|
||||
|
||||
def action_refresh_screen(self) -> None:
|
||||
"""Refresh the screen"""
|
||||
self.update_table()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user