diff --git a/data/download_queue.json b/data/download_queue.json index 7467f94..e9d8295 100644 --- a/data/download_queue.json +++ b/data/download_queue.json @@ -13,8 +13,8 @@ "status": "cancelled", "priority": "NORMAL", "added_at": "2025-11-20T17:12:34.485225Z", - "started_at": "2025-11-20T18:09:04.527701Z", - "completed_at": "2025-11-20T18:09:27.852314Z", + "started_at": "2025-11-20T18:15:48.216607Z", + "completed_at": "2025-11-20T18:16:38.929297Z", "progress": null, "error": null, "retry_count": 0, @@ -943,5 +943,5 @@ ], "active": [], "failed": [], - "timestamp": "2025-11-20T18:09:27.852598+00:00" + "timestamp": "2025-11-20T18:16:38.929570+00:00" } \ No newline at end of file diff --git a/src/core/SeriesApp.py b/src/core/SeriesApp.py index 9749412..54e7cc6 100644 --- a/src/core/SeriesApp.py +++ b/src/core/SeriesApp.py @@ -260,6 +260,7 @@ class SeriesApp: progress=(downloaded / total_bytes) * 100 if total_bytes else 0, eta=eta, mbper_sec=mbper_sec, + item_id=item_id, ) ) # Perform download in thread to avoid blocking event loop diff --git a/src/server/services/anime_service.py b/src/server/services/anime_service.py index 1bee5af..4045bdd 100644 --- a/src/server/services/anime_service.py +++ b/src/server/services/anime_service.py @@ -37,6 +37,7 @@ class AnimeService: self._app = series_app self._directory = series_app.directory_to_search self._progress_service = progress_service or get_progress_service() + self._event_loop: Optional[asyncio.AbstractEventLoop] = None # Subscribe to SeriesApp events # Note: Events library uses assignment (=), not += operator try: @@ -54,13 +55,17 @@ class AnimeService: args: DownloadStatusEventArgs from SeriesApp """ try: - # Check if there's a running event loop + # Get event loop - try running loop first, then stored loop + loop = None try: loop = asyncio.get_running_loop() except RuntimeError: - # No running loop - log and skip + # No running loop in this thread - use stored loop + loop = self._event_loop + + if not loop: logger.debug( - "No running event loop for download status event", + "No event loop available for download status event", status=args.status ) return @@ -74,38 +79,42 @@ class AnimeService: # Map SeriesApp download events to progress service if args.status == "started": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.start_progress( progress_id=progress_id, progress_type=ProgressType.DOWNLOAD, title=f"Downloading {args.serie_folder}", message=f"S{args.season:02d}E{args.episode:02d}", metadata={"item_id": args.item_id} if args.item_id else None, - ) + ), + loop ) elif args.status == "progress": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.update_progress( progress_id=progress_id, current=int(args.progress), total=100, message=args.message or "Downloading...", metadata={"item_id": args.item_id} if args.item_id else None, - ) + ), + loop ) elif args.status == "completed": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.complete_progress( progress_id=progress_id, message="Download completed", - ) + ), + loop ) elif args.status == "failed": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.fail_progress( progress_id=progress_id, error_message=args.message or str(args.error), - ) + ), + loop ) except Exception as exc: logger.error( @@ -122,56 +131,65 @@ class AnimeService: try: scan_id = "library_scan" - # Check if there's a running event loop + # Get event loop - try running loop first, then stored loop + loop = None try: loop = asyncio.get_running_loop() except RuntimeError: - # No running loop - log and skip + # No running loop in this thread - use stored loop + loop = self._event_loop + + if not loop: logger.debug( - "No running event loop for scan status event", + "No event loop available for scan status event", status=args.status ) return # Map SeriesApp scan events to progress service if args.status == "started": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.start_progress( progress_id=scan_id, progress_type=ProgressType.SCAN, title="Scanning anime library", message=args.message or "Initializing scan...", - ) + ), + loop ) elif args.status == "progress": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.update_progress( progress_id=scan_id, current=args.current, total=args.total, message=args.message or f"Scanning: {args.folder}", - ) + ), + loop ) elif args.status == "completed": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.complete_progress( progress_id=scan_id, message=args.message or "Scan completed", - ) + ), + loop ) elif args.status == "failed": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.fail_progress( progress_id=scan_id, error_message=args.message or str(args.error), - ) + ), + loop ) elif args.status == "cancelled": - loop.create_task( + asyncio.run_coroutine_threadsafe( self._progress_service.fail_progress( progress_id=scan_id, error_message=args.message or "Scan cancelled", - ) + ), + loop ) except Exception as exc: logger.error("Error handling scan status event", error=str(exc)) @@ -228,6 +246,9 @@ class AnimeService: forwarded to the ProgressService through event handlers. """ try: + # Store event loop for event handlers + self._event_loop = asyncio.get_running_loop() + # SeriesApp.rescan is now async and handles events internally await self._app.rescan() @@ -247,21 +268,33 @@ class AnimeService: season: int, episode: int, key: str, + item_id: Optional[str] = None, ) -> bool: """Start a download. The SeriesApp now handles progress tracking via events which are forwarded to the ProgressService through event handlers. + Args: + serie_folder: Serie folder name + season: Season number + episode: Episode number + key: Serie key + item_id: Optional download queue item ID for tracking + Returns True on success or raises AnimeServiceError on failure. """ try: + # Store event loop for event handlers + self._event_loop = asyncio.get_running_loop() + # SeriesApp.download is now async and handles events internally return await self._app.download( serie_folder=serie_folder, season=season, episode=episode, key=key, + item_id=item_id, ) except Exception as exc: logger.exception("download failed") diff --git a/src/server/services/download_service.py b/src/server/services/download_service.py index bccc440..f5c4c46 100644 --- a/src/server/services/download_service.py +++ b/src/server/services/download_service.py @@ -808,6 +808,7 @@ class DownloadService: season=item.episode.season, episode=item.episode.episode, key=item.serie_id, + item_id=item.id, ) # Handle result diff --git a/tests/unit/test_anime_service.py b/tests/unit/test_anime_service.py index ec4a9b7..e7af89a 100644 --- a/tests/unit/test_anime_service.py +++ b/tests/unit/test_anime_service.py @@ -247,6 +247,7 @@ class TestDownload: season=1, episode=1, key="test_key", + item_id=None, ) @pytest.mark.asyncio @@ -272,6 +273,7 @@ class TestDownload: season=1, episode=1, key="test_key", + item_id=None, ) @pytest.mark.asyncio