#!/usr/bin/env python3 """ Download Queue Service for YouTube TUI Manages the queue of videos to download """ import json import logging from logging.handlers import RotatingFileHandler 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() # Configure logging LOG_DIR = Path.home() / ".config" / "youtube_cli" / "logs" LOG_DIR.mkdir(parents=True, exist_ok=True) LOG_FILE = LOG_DIR / "app.log" # Use RotatingFileHandler for log rotation (10MB, 5 backups) 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", ) ) # Create console handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter("%(message)s")) # Configure root logger logging.basicConfig( level=logging.DEBUG, handlers=[ file_handler, console_handler, ], ) logger = logging.getLogger(__name__) 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: logger.warning(f"Error loading queue: {e}") 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: logger.warning(f"Error saving queue: {e}") 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, }