269 lines
8.4 KiB
Python
269 lines
8.4 KiB
Python
#!/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,
|
|
}
|