Jarian Cottingham 06877fb2fc feat: add TUI (Textual User Interface) with download queue system
- New TUI using Textual framework with responsive interface
- Search, results, download, and queue screens for YouTube video management
- Download queue system with sequential background downloads
- Status bar with queue count and download progress indicators
- Comprehensive test suite (97 tests) with 100% pass rate
- Modern Python packaging with pyproject.toml and uv support
- Added requirements-tui.txt for TUI-specific dependencies
- Updated setup.py and install.sh for TUI integration
- Enhanced README.md with TUI usage documentation
2026-02-25 15:59:15 -06:00

237 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""
Download Queue Service for YouTube TUI
Manages the queue of videos to download
"""
import json
from pathlib import Path
from typing import List, Optional
from rich.console import Console
from youtube_tui.models.queue_item import QueueItem, QueueStatus
from youtube_tui.models.video import Video
console = Console()
class DownloadQueue:
"""Manages the download queue"""
_archive_file = Path.home() / ".config" / "youtube_cli" / "download_queue.json"
def __init__(self) -> None:
self._queue: List[QueueItem] = []
self._load_queue()
def _load_queue(self) -> None:
"""Load queue from archive file"""
try:
if self._archive_file.exists():
with open(self._archive_file, "r") as f:
data = json.load(f)
self._queue = [QueueItem.from_dict(item) for item in data]
except Exception as e:
console.print(f"[yellow]Error loading queue: {e}[/yellow]")
self._queue = []
def _save_queue(self) -> None:
"""Save queue to archive file"""
try:
self._archive_file.parent.mkdir(parents=True, exist_ok=True)
with open(self._archive_file, "w") as f:
data = [item.to_dict() for item in self._queue]
json.dump(data, f, indent=2)
except Exception as e:
console.print(f"[yellow]Error saving queue: {e}[/yellow]")
def add_video(
self,
video: Video,
category: Optional[str] = None,
network_folder: Optional[str] = None,
) -> QueueItem:
"""Add a video to the queue"""
item = QueueItem(video=video, category=category, network_folder=network_folder)
self._queue.append(item)
self._save_queue()
return item
def remove_item(self, item_id: str) -> bool:
"""Remove a queue item by its UUID string"""
try:
import uuid
item_uuid = uuid.UUID(item_id)
except (ValueError, TypeError):
return False
for i, item in enumerate(self._queue):
if item.id == item_uuid:
del self._queue[i]
self._save_queue()
return True
return False
def get_next_pending(self) -> Optional[QueueItem]:
"""Get the next pending video to download"""
for item in self._queue:
if item.status == QueueStatus.PENDING:
return item
return None
def update_item_status(self, item_id: str, status: QueueStatus) -> None:
"""Update the status of a queue item by UUID string"""
try:
import uuid
item_uuid = uuid.UUID(item_id)
except (ValueError, TypeError):
return
for item in self._queue:
if item.id == item_uuid:
item.status = status
self._save_queue()
return
def update_progress(self, item_id: str, percentage: int) -> None:
"""Update the progress of a queue item by UUID string"""
try:
import uuid
item_uuid = uuid.UUID(item_id)
except (ValueError, TypeError):
return
for item in self._queue:
if item.id == item_uuid:
item.update_progress(percentage)
self._save_queue()
return
def get_all_items(self) -> List[QueueItem]:
"""Get all queue items"""
return self._queue.copy()
def get_active_count(self) -> int:
"""Get count of active items (pending + downloading)"""
return sum(
1
for item in self._queue
if item.status in (QueueStatus.PENDING, QueueStatus.DOWNLOADING)
)
def get_pending_count(self) -> int:
"""Get count of pending items"""
return sum(1 for item in self._queue if item.status == QueueStatus.PENDING)
def cancel_item(self, item_id: str) -> None:
"""Cancel a queue item by UUID string"""
try:
import uuid
item_uuid = uuid.UUID(item_id)
except (ValueError, TypeError):
return
for item in self._queue:
if item.id == item_uuid:
item.cancel()
self._save_queue()
return
def clear_completed(self) -> int:
"""Remove completed and cancelled items from queue"""
initial_count = len(self._queue)
self._queue = [
item
for item in self._queue
if item.status not in (QueueStatus.COMPLETED, QueueStatus.CANCELLED)
]
removed = initial_count - len(self._queue)
if removed > 0:
self._save_queue()
return removed
def clear_failed(self) -> int:
"""Remove failed items from queue"""
initial_count = len(self._queue)
self._queue = [
item for item in self._queue if item.status != QueueStatus.FAILED
]
removed = initial_count - len(self._queue)
if removed > 0:
self._save_queue()
return removed
def get_downloading_item(self) -> Optional[QueueItem]:
"""Get the currently downloading item"""
for item in self._queue:
if item.status == QueueStatus.DOWNLOADING:
return item
return None
def remove_video(self, video_id: str) -> bool:
"""Remove a video from the queue by video ID (alias for remove_by_video_id)"""
return self.remove_by_video_id(video_id)
def update_status(
self, video_id: str, status: QueueStatus, progress: Optional[int] = None
) -> None:
"""Update the status of a video in the queue by video ID"""
for item in self._queue:
if item.video and item.video.video_id == video_id:
item.status = status
if progress is not None:
item.update_progress(progress)
self._save_queue()
return
def cancel_video(self, video_id: str) -> bool:
"""Cancel a video in the queue by video ID"""
for item in self._queue:
if item.video and item.video.video_id == video_id:
item.cancel()
self._save_queue()
return True
return False
def remove_by_video_id(self, video_id: str) -> bool:
"""Remove a video from the queue by video ID"""
for i, item in enumerate(self._queue):
if item.video and item.video.video_id == video_id:
del self._queue[i]
self._save_queue()
return True
return False
def get_queue(self) -> List[QueueItem]:
"""Get all queue items (alias for get_all_items)"""
return self.get_all_items()
def get_stats(self) -> dict:
"""Get queue statistics"""
total = len(self._queue)
pending = sum(1 for item in self._queue if item.status == QueueStatus.PENDING)
downloading = sum(
1 for item in self._queue if item.status == QueueStatus.DOWNLOADING
)
completed = sum(
1 for item in self._queue if item.status == QueueStatus.COMPLETED
)
cancelled = sum(
1 for item in self._queue if item.status == QueueStatus.CANCELLED
)
failed = sum(1 for item in self._queue if item.status == QueueStatus.FAILED)
return {
"total": total,
"pending": pending,
"downloading": downloading,
"completed": completed,
"cancelled": cancelled,
"failed": failed,
}