fix: load search results in ResultsScreen on mount
This commit is contained in:
parent
06877fb2fc
commit
c22b3884bd
418
TUI.md
Normal file
418
TUI.md
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
# YouTube TUI Documentation
|
||||||
|
|
||||||
|
A modern, terminal-based user interface for browsing and downloading YouTube videos using Textual and yt-dlp.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Modern Terminal Interface** - Built with Textual for a rich, interactive experience
|
||||||
|
- **Search Videos** - Search YouTube with keyword queries and view results in a table
|
||||||
|
- **Download Videos** - Download videos directly with progress indication
|
||||||
|
- **Playlist Support** - Download entire YouTube playlists
|
||||||
|
- **Pagination** - Navigate through search results with keyboard shortcuts
|
||||||
|
- **Category Selection** - Choose download locations from configured categories
|
||||||
|
- **Network Share Integration** - Automatically copy downloads to network shares
|
||||||
|
- **Download Archive** - Track already downloaded videos
|
||||||
|
- **Short Video Detection** - Automatically detect and mark short videos
|
||||||
|
- **Keyboard Navigation** - Full keyboard control with intuitive shortcuts
|
||||||
|
- **Responsive Design** - Works in any terminal size with adaptive layout
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- Textual (TUI framework)
|
||||||
|
- yt-dlp
|
||||||
|
- rich
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/youtube-cli.git
|
||||||
|
cd youtube-cli
|
||||||
|
pip install -e .[tui,dev]
|
||||||
|
```
|
||||||
|
|
||||||
|
### With pip
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install youtube-cli[tui]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using pip with all extras
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install youtube-cli[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install:
|
||||||
|
- Core dependencies (yt-dlp, rich, requests)
|
||||||
|
- TUI dependencies (textual)
|
||||||
|
- Development tools (pytest, black, ruff, mypy)
|
||||||
|
|
||||||
|
### Docker Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t youtube-tui -f docker/Dockerfile.tui .
|
||||||
|
docker run -it --rm -v ~/Downloads:/root/Downloads youtube-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
youtube-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running with Arguments
|
||||||
|
|
||||||
|
```bash
|
||||||
|
youtube-tui "python tutorial"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open the TUI and immediately search for "python tutorial".
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
### Global Shortcuts
|
||||||
|
|
||||||
|
| Key | Action | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| `Ctrl+C` | Quit | Exit the application |
|
||||||
|
| `Ctrl+Q` | Quit | Exit the application |
|
||||||
|
| `F1` | Help | Open help screen |
|
||||||
|
| `Esc` | Back | Go back to previous screen |
|
||||||
|
| `Ctrl+R` | Refresh | Refresh current screen |
|
||||||
|
|
||||||
|
### Search Screen
|
||||||
|
|
||||||
|
| Key | Action | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| `Enter` | Search | Execute search with current input |
|
||||||
|
| `Ctrl+F` | Search | Open search from anywhere |
|
||||||
|
| `Escape` | Cancel | Go back to previous screen |
|
||||||
|
|
||||||
|
### Results Screen
|
||||||
|
|
||||||
|
| Key | Action | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| `Enter` | Download | Download selected video |
|
||||||
|
| `n` | Next Page | Go to next page of results |
|
||||||
|
| `p` | Previous Page | Go to previous page of results |
|
||||||
|
| `q` | Quit | Go back to search screen |
|
||||||
|
| `Escape` | Quit | Go back to search screen |
|
||||||
|
| `Ctrl+R` | Refresh | Refresh current results |
|
||||||
|
| `Ctrl+F` | Search | Open search from anywhere |
|
||||||
|
|
||||||
|
### Download Screen
|
||||||
|
|
||||||
|
| Key | Action | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| `Escape` | Cancel | Cancel current download |
|
||||||
|
| `Ctrl+R` | Refresh | Refresh screen |
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### Moving Between Screens
|
||||||
|
|
||||||
|
1. **From Search to Results**: Enter a search term and press Enter
|
||||||
|
2. **From Results to Download**: Select a video and press Enter
|
||||||
|
3. **From Download to Results**: Wait for download to complete or press Escape to cancel
|
||||||
|
4. **From Any Screen to Search**: Press Escape or use the navigation menu
|
||||||
|
|
||||||
|
### Page Navigation
|
||||||
|
|
||||||
|
- **Next Page**: Press `n` or click the "Next" button
|
||||||
|
- **Previous Page**: Press `p` or click the "Previous" button
|
||||||
|
- **Page Indicator**: Shows current page and total pages
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The TUI reads configuration from the same config file as the CLI:
|
||||||
|
|
||||||
|
**Location**: `~/.config/youtube_cli/config.json`
|
||||||
|
|
||||||
|
**Example Configuration**:
|
||||||
|
|
||||||
|
```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"
|
||||||
|
},
|
||||||
|
"network_share_path": "/Volumes/MediaServer/Youtube/",
|
||||||
|
"default_network_subfolder": "General"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `download_dir` | Default download directory | `~/Downloads/youtube` |
|
||||||
|
| `default_locations` | List of category folders | `["~/Downloads/youtube"]` |
|
||||||
|
| `max_videos_per_page` | Number of videos per page | `15` |
|
||||||
|
| `yt_dlp_args` | Custom yt-dlp arguments | `{"format": "bestvideo[height=1080]+bestaudio"}` |
|
||||||
|
| `network_share_path` | Network share path | `/Volumes/MediaServer/Youtube/` |
|
||||||
|
| `default_network_subfolder` | Default network subfolder | `General` |
|
||||||
|
|
||||||
|
## Screens
|
||||||
|
|
||||||
|
### Search Screen
|
||||||
|
|
||||||
|
The search screen is the main entry point. Enter a search term and press Enter to search YouTube.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Search input field with placeholder text
|
||||||
|
- Search and Cancel buttons
|
||||||
|
- Status bar showing current screen and mode
|
||||||
|
|
||||||
|
### Results Screen
|
||||||
|
|
||||||
|
Displays search results in a table format with:
|
||||||
|
|
||||||
|
- **Video Title**: Click to select for download
|
||||||
|
- **Author/Channel**: Shows the video creator
|
||||||
|
- **Duration**: Video length (MM:SS format)
|
||||||
|
- **Type**: Video, Short, or Playlist indicator
|
||||||
|
|
||||||
|
**Table Features**:
|
||||||
|
- Keyboard navigation (arrow keys)
|
||||||
|
- Selection highlighting
|
||||||
|
- Auto-truncation of long text
|
||||||
|
- Short video detection with "(short)" prefix
|
||||||
|
|
||||||
|
### Download Screen
|
||||||
|
|
||||||
|
Shows download progress with:
|
||||||
|
|
||||||
|
- **Video Title**: Current video being downloaded
|
||||||
|
- **Progress Bar**: Visual progress indicator
|
||||||
|
- **Status Message**: Current download state
|
||||||
|
- **Cancel Button**: Cancel ongoing download
|
||||||
|
|
||||||
|
**States**:
|
||||||
|
- Preparing: Initial setup
|
||||||
|
- Downloading: Active download in progress
|
||||||
|
- Complete: Download finished successfully
|
||||||
|
- Failed: Download encountered an error
|
||||||
|
|
||||||
|
### Help Screen
|
||||||
|
|
||||||
|
Displays help information with:
|
||||||
|
|
||||||
|
- Keyboard shortcuts reference
|
||||||
|
- Configuration options
|
||||||
|
- Troubleshooting tips
|
||||||
|
- Version information
|
||||||
|
|
||||||
|
## Status Bar
|
||||||
|
|
||||||
|
The status bar appears at the bottom of each screen and shows:
|
||||||
|
|
||||||
|
- Current screen name
|
||||||
|
- Application version
|
||||||
|
- yt-dlp version
|
||||||
|
- Status message
|
||||||
|
- Current time
|
||||||
|
- Active theme
|
||||||
|
|
||||||
|
## Theming
|
||||||
|
|
||||||
|
The TUI supports theming through Textual's CSS system. You can customize colors and styles by creating a custom CSS file.
|
||||||
|
|
||||||
|
### Default Theme
|
||||||
|
|
||||||
|
The default theme uses a modern dark color scheme with:
|
||||||
|
|
||||||
|
- Dark background colors
|
||||||
|
- Bright text for readability
|
||||||
|
- Primary color accents
|
||||||
|
- Muted colors for secondary elements
|
||||||
|
|
||||||
|
### Custom Themes
|
||||||
|
|
||||||
|
Create a custom theme by editing the TUI's CSS file or using Textual's theme system.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Application Won't Start
|
||||||
|
|
||||||
|
**Issue**: `ModuleNotFoundError: No module named 'textual'`
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
pip install textual
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Returns No Results
|
||||||
|
|
||||||
|
**Issue**: Search completes but no videos are found
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. Network connectivity issues
|
||||||
|
2. YouTube API restrictions
|
||||||
|
3. Invalid search term
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check internet connection
|
||||||
|
2. Try a different search term
|
||||||
|
3. Check yt-dlp installation: `yt-dlp --version`
|
||||||
|
|
||||||
|
### Download Fails
|
||||||
|
|
||||||
|
**Issue**: Download starts but fails partway through
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. Network interruption
|
||||||
|
2. Video unavailable
|
||||||
|
3. Storage space issues
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check network connection
|
||||||
|
2. Verify video URL works in browser
|
||||||
|
3. Check available disk space
|
||||||
|
|
||||||
|
### Keyboard Shortcuts Not Working
|
||||||
|
|
||||||
|
**Issue**: Keyboard shortcuts don't respond
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Ensure terminal supports keyboard events
|
||||||
|
2. Check for terminal-specific key bindings
|
||||||
|
3. Try different terminal application
|
||||||
|
|
||||||
|
### Screen Displays Incorrectly
|
||||||
|
|
||||||
|
**Issue**: Text appears garbled or misaligned
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Resize terminal window
|
||||||
|
2. Check terminal encoding (UTF-8 recommended)
|
||||||
|
3. Update Textual: `pip install --upgrade textual`
|
||||||
|
|
||||||
|
### Configuration Changes Not Applied
|
||||||
|
|
||||||
|
**Issue**: Changes to config file don't take effect
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Restart the application
|
||||||
|
2. Check config file permissions
|
||||||
|
3. Verify JSON syntax is valid
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Download Multiple Videos
|
||||||
|
|
||||||
|
1. Search for videos
|
||||||
|
2. Select multiple videos by navigating and using the download function
|
||||||
|
3. The TUI will queue downloads sequentially
|
||||||
|
|
||||||
|
### Download Playlists
|
||||||
|
|
||||||
|
1. Search for a playlist URL
|
||||||
|
2. Select the playlist from results
|
||||||
|
3. The TUI will download all videos in the playlist
|
||||||
|
|
||||||
|
### Custom Download Locations
|
||||||
|
|
||||||
|
1. Press `Ctrl+F` to open search from anywhere
|
||||||
|
2. Search for a video
|
||||||
|
3. When downloading, select a custom category
|
||||||
|
4. The video will be saved to that category's folder
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
The TUI can also be used with the REST API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the API server
|
||||||
|
python app.py
|
||||||
|
|
||||||
|
# Use the API
|
||||||
|
curl "http://localhost:4096/search?q=python&page=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
python test_tui.py
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
pytest --cov=youtube_tui tests/
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/unit/test_models.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building the TUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install development dependencies
|
||||||
|
pip install -e .[dev]
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
ruff check .
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
black .
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
mypy .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
youtube_tui/
|
||||||
|
├── app.py # Main application class
|
||||||
|
├── __main__.py # Entry point
|
||||||
|
├── models/ # Data models
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── video.py
|
||||||
|
├── services/ # Service layer
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── youtube.py
|
||||||
|
├── screens/ # Textual screens
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── search.py
|
||||||
|
│ ├── results.py
|
||||||
|
│ ├── download.py
|
||||||
|
│ └── help.py
|
||||||
|
└── widgets/ # Custom widgets
|
||||||
|
├── __init__.py
|
||||||
|
├── status_bar.py
|
||||||
|
└── command_palette.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Make your changes
|
||||||
|
4. Run tests (`python test_tui.py`)
|
||||||
|
5. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
6. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
7. Open a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- Built with [Textual](https://textual.textualize.io/) - A TUI framework for Python
|
||||||
|
- Uses [yt-dlp](https://github.com/yt-dlp/yt-dlp) for YouTube video handling
|
||||||
|
- Inspired by [Rich](https://github.com/Textualize/rich) for terminal formatting
|
||||||
@ -1,3 +1,20 @@
|
|||||||
yt-dlp
|
# Core dependencies
|
||||||
rich
|
yt-dlp>=2023.12.0
|
||||||
requests
|
rich>=13.0.0
|
||||||
|
requests>=2.28.0
|
||||||
|
|
||||||
|
# Testing dependencies
|
||||||
|
pytest>=7.0.0
|
||||||
|
pytest-asyncio>=0.21.0
|
||||||
|
pytest-cov>=4.0.0
|
||||||
|
|
||||||
|
# TUI dependencies (for YouTube TUI)
|
||||||
|
textual>=0.40.0
|
||||||
|
|
||||||
|
# Development dependencies
|
||||||
|
black>=23.0.0
|
||||||
|
ruff>=0.1.0
|
||||||
|
mypy>=1.0.0
|
||||||
|
|
||||||
|
# API dependencies
|
||||||
|
Flask==2.3.3
|
||||||
@ -3,6 +3,7 @@
|
|||||||
Results Screen for YouTube TUI
|
Results Screen for YouTube TUI
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
@ -119,18 +120,18 @@ class ResultsScreen(Screen):
|
|||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Called when screen is mounted"""
|
"""Called when screen is mounted"""
|
||||||
# Note: load_results is async but on_mount is sync
|
# Use asyncio.create_task to run the async load_results method
|
||||||
# This is a limitation of Textual's on_mount
|
# since on_mount is synchronous but we need to fetch data asynchronously
|
||||||
# For now, just update status without loading
|
self.load_task = asyncio.create_task(self.load_results())
|
||||||
self.update_status(
|
self.update_status("[blue]Loading search results...[/blue]")
|
||||||
f"[green]Loaded {len(self.videos)} videos[/green] - Press 'n' for next page, 'p' for previous"
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_refresh_screen(self) -> None:
|
def action_refresh_screen(self) -> None:
|
||||||
"""Refresh the screen"""
|
"""Refresh the screen"""
|
||||||
# Note: This is called from app which doesn't await
|
# Cancel any existing load task and start a new one
|
||||||
# For now, just update status without reloading
|
if hasattr(self, "load_task") and self.load_task:
|
||||||
self.update_status(f"[blue]Refreshed: {len(self.videos)} videos[/blue]")
|
self.load_task.cancel()
|
||||||
|
self.load_task = asyncio.create_task(self.load_results())
|
||||||
|
self.update_status("[blue]Refreshing results...[/blue]")
|
||||||
|
|
||||||
def action_search_from_anywhere(self) -> None:
|
def action_search_from_anywhere(self) -> None:
|
||||||
"""Open search from anywhere"""
|
"""Open search from anywhere"""
|
||||||
@ -269,14 +270,20 @@ class ResultsScreen(Screen):
|
|||||||
"""Go to next page"""
|
"""Go to next page"""
|
||||||
if self.page < self.total_pages or len(self.videos) >= self.max_per_page:
|
if self.page < self.total_pages or len(self.videos) >= self.max_per_page:
|
||||||
self.page += 1
|
self.page += 1
|
||||||
# For now, just update status without reloading (async)
|
# Cancel any existing load task and start a new one
|
||||||
|
if hasattr(self, "load_task") and self.load_task:
|
||||||
|
self.load_task.cancel()
|
||||||
|
self.load_task = asyncio.create_task(self.load_results())
|
||||||
self.update_status(f"[blue]Loading page {self.page}...[/blue]")
|
self.update_status(f"[blue]Loading page {self.page}...[/blue]")
|
||||||
|
|
||||||
def action_previous_page(self) -> None:
|
def action_previous_page(self) -> None:
|
||||||
"""Go to previous page"""
|
"""Go to previous page"""
|
||||||
if self.page > 1:
|
if self.page > 1:
|
||||||
self.page -= 1
|
self.page -= 1
|
||||||
# For now, just update status without reloading (async)
|
# Cancel any existing load task and start a new one
|
||||||
|
if hasattr(self, "load_task") and self.load_task:
|
||||||
|
self.load_task.cancel()
|
||||||
|
self.load_task = asyncio.create_task(self.load_results())
|
||||||
self.update_status(f"[blue]Loading page {self.page}...[/blue]")
|
self.update_status(f"[blue]Loading page {self.page}...[/blue]")
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user