fix progress events

This commit is contained in:
2025-11-07 18:40:36 +01:00
parent 5c4bd3d7e8
commit 2441730862
5 changed files with 673 additions and 249 deletions

View File

@@ -13,14 +13,13 @@ from collections import deque
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Dict, List, Optional
from typing import Dict, List, Optional
import structlog
from src.server.models.download import (
DownloadItem,
DownloadPriority,
DownloadProgress,
DownloadStatus,
EpisodeIdentifier,
QueueStats,
@@ -82,37 +81,33 @@ class DownloadService:
# Executor for blocking operations
self._executor = ThreadPoolExecutor(max_workers=1)
# WebSocket broadcast callback
self._broadcast_callback: Optional[Callable] = None
# Statistics tracking
self._total_downloaded_mb: float = 0.0
self._download_speeds: deque[float] = deque(maxlen=10)
# Subscribe to SeriesApp download events for progress tracking
# Note: Events library uses assignment (=), not += operator
if hasattr(anime_service, '_app') and hasattr(
anime_service._app, 'download_status'
):
# Save existing handler if any, and chain them
existing_handler = anime_service._app.download_status
if existing_handler:
def chained_handler(args):
existing_handler(args)
self._on_seriesapp_download_status(args)
anime_service._app.download_status = chained_handler
else:
anime_service._app.download_status = (
self._on_seriesapp_download_status
)
# Load persisted queue
self._load_queue()
# Initialize queue progress tracking
asyncio.create_task(self._init_queue_progress())
logger.info(
"DownloadService initialized",
max_retries=max_retries,
)
async def _init_queue_progress(self) -> None:
"""Initialize the download queue progress tracking."""
try:
from src.server.services.progress_service import ProgressType
await self._progress_service.start_progress(
progress_id="download_queue",
progress_type=ProgressType.QUEUE,
title="Download Queue",
message="Queue ready",
)
except Exception as e:
logger.error("Failed to initialize queue progress", error=str(e))
def _add_to_pending_queue(
self, item: DownloadItem, front: bool = False
@@ -154,91 +149,6 @@ class DownloadService:
except (ValueError, KeyError):
return None
def set_broadcast_callback(self, callback: Callable) -> None:
"""Set callback for broadcasting status updates via WebSocket."""
self._broadcast_callback = callback
logger.debug("Broadcast callback registered")
def _on_seriesapp_download_status(self, args) -> None:
"""Handle download status events from SeriesApp.
Updates the active download item with progress information.
Args:
args: DownloadStatusEventArgs from SeriesApp
"""
try:
# Only process if we have an active download
if not self._active_download:
return
# Match the event to the active download item
# SeriesApp events include serie_folder, season, episode
if (
self._active_download.serie_folder == args.serie_folder
and self._active_download.episode.season == args.season
and self._active_download.episode.episode == args.episode
):
if args.status == "progress":
# Update item progress
self._active_download.progress = DownloadProgress(
percent=args.progress,
downloaded_mb=(
args.progress * args.mbper_sec / 100
if args.mbper_sec
else 0.0
),
total_mb=None, # Not provided by SeriesApp
speed_mbps=args.mbper_sec,
eta_seconds=args.eta,
)
# Track speed
if args.mbper_sec:
self._download_speeds.append(args.mbper_sec)
# Broadcast update
asyncio.create_task(
self._broadcast_update(
"download_progress",
{
"download_id": self._active_download.id,
"item_id": self._active_download.id,
"serie_name": self._active_download.serie_name,
"season": args.season,
"episode": args.episode,
"progress": (
self._active_download.progress.model_dump(
mode="json"
)
),
},
)
)
except Exception as exc:
logger.error(
"Error handling SeriesApp download status",
error=str(exc)
)
async def _broadcast_update(self, update_type: str, data: dict) -> None:
"""Broadcast update to connected WebSocket clients.
Args:
update_type: Type of update (download_progress, queue_status, etc.)
data: Update data to broadcast
"""
if self._broadcast_callback:
try:
await self._broadcast_callback(update_type, data)
except Exception as e:
logger.error(
"Failed to broadcast update",
update_type=update_type,
error=str(e),
)
def _generate_item_id(self) -> str:
"""Generate unique identifier for download items."""
return str(uuid.uuid4())
@@ -359,15 +269,17 @@ class DownloadService:
self._save_queue()
# Broadcast queue status update
# Notify via progress service
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message=f"Added {len(created_ids)} items to queue",
metadata={
"action": "items_added",
"added_ids": created_ids,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
return created_ids
@@ -416,15 +328,17 @@ class DownloadService:
if removed_ids:
self._save_queue()
# Broadcast queue status update
# Notify via progress service
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message=f"Removed {len(removed_ids)} items from queue",
metadata={
"action": "items_removed",
"removed_ids": removed_ids,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
return removed_ids
@@ -498,17 +412,24 @@ class DownloadService:
remaining=len(self._pending_queue)
)
# Broadcast queue status update
# Notify via progress service
queue_status = await self.get_queue_status()
await self._broadcast_update(
"download_started",
{
msg = (
f"Started: {item.serie_name} "
f"S{item.episode.season:02d}E{item.episode.episode:02d}"
)
await self._progress_service.update_progress(
progress_id="download_queue",
message=msg,
metadata={
"action": "download_started",
"item_id": item.id,
"serie_name": item.serie_name,
"season": item.episode.season,
"episode": item.episode.episode,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
# Process the download (this will wait until complete)
@@ -532,11 +453,11 @@ class DownloadService:
if len(self._pending_queue) == 0:
logger.info("Queue processing completed - all items processed")
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_completed",
{
"message": "All downloads completed",
"queue_status": queue_status.model_dump(mode="json"),
await self._progress_service.complete_progress(
progress_id="download_queue",
message="All downloads completed",
metadata={
"queue_status": queue_status.model_dump(mode="json")
},
)
else:
@@ -561,14 +482,17 @@ class DownloadService:
self._is_stopped = True
logger.info("Download processing stopped")
# Broadcast queue status update
# Notify via progress service
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_stopped",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message="Queue processing stopped",
metadata={
"action": "queue_stopped",
"is_stopped": True,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
async def get_queue_status(self) -> QueueStatus:
@@ -638,16 +562,18 @@ class DownloadService:
self._completed_items.clear()
logger.info("Cleared completed items", count=count)
# Broadcast queue status update
# Notify via progress service
if count > 0:
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message=f"Cleared {count} completed items",
metadata={
"action": "completed_cleared",
"cleared_count": count,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
return count
@@ -662,16 +588,18 @@ class DownloadService:
self._failed_items.clear()
logger.info("Cleared failed items", count=count)
# Broadcast queue status update
# Notify via progress service
if count > 0:
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message=f"Cleared {count} failed items",
metadata={
"action": "failed_cleared",
"cleared_count": count,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
return count
@@ -690,16 +618,18 @@ class DownloadService:
# Save queue state
self._save_queue()
# Broadcast queue status update
# Notify via progress service
if count > 0:
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message=f"Cleared {count} pending items",
metadata={
"action": "pending_cleared",
"cleared_count": count,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
return count
@@ -746,15 +676,17 @@ class DownloadService:
if retried_ids:
self._save_queue()
# Broadcast queue status update
# Notify via progress service
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
await self._progress_service.update_progress(
progress_id="download_queue",
message=f"Retried {len(retried_ids)} failed items",
metadata={
"action": "items_retried",
"retried_ids": retried_ids,
"queue_status": queue_status.model_dump(mode="json"),
},
force_broadcast=True,
)
return retried_ids
@@ -786,10 +718,10 @@ class DownloadService:
)
# Execute download via anime service
# Note: AnimeService handles progress via SeriesApp events
# Progress updates received via _on_seriesapp_download_status
# Use serie_folder if available, otherwise fall back to serie_id
# for backwards compatibility with old queue items
# AnimeService handles ALL progress via SeriesApp events:
# - download started/progress/completed/failed events
# - All updates forwarded to ProgressService
# - ProgressService broadcasts to WebSocket clients
folder = item.serie_folder if item.serie_folder else item.serie_id
success = await self._anime_service.download(
serie_folder=folder,
@@ -812,21 +744,6 @@ class DownloadService:
logger.info(
"Download completed successfully", item_id=item.id
)
# Broadcast completion (progress already handled by events)
await self._broadcast_update(
"download_complete",
{
"download_id": item.id,
"item_id": item.id,
"serie_name": item.serie_name,
"season": item.episode.season,
"episode": item.episode.episode,
"downloaded_mb": item.progress.downloaded_mb
if item.progress
else 0,
},
)
else:
raise AnimeServiceError("Download returned False")
@@ -843,20 +760,8 @@ class DownloadService:
error=str(e),
retry_count=item.retry_count,
)
# Broadcast failure (progress already handled by events)
await self._broadcast_update(
"download_failed",
{
"download_id": item.id,
"item_id": item.id,
"serie_name": item.serie_name,
"season": item.episode.season,
"episode": item.episode.episode,
"error": item.error,
"retry_count": item.retry_count,
},
)
# Note: Failure is already broadcast by AnimeService
# via ProgressService when SeriesApp fires failed event
finally:
# Remove from active downloads