From 9fce61794933c1d0c6fe9df436d60a8e4d46e88c Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 1 Nov 2025 18:46:53 +0100 Subject: [PATCH] fix percentage --- data/download_queue.json | 42 +------------ src/core/SeriesApp.py | 9 +++ src/core/providers/aniworld_provider.py | 43 +++++++++++-- src/server/services/download_service.py | 80 ++++++++++++++++++------- 4 files changed, 106 insertions(+), 68 deletions(-) diff --git a/data/download_queue.json b/data/download_queue.json index cdbb2f3..c9d97cc 100644 --- a/data/download_queue.json +++ b/data/download_queue.json @@ -1,45 +1,5 @@ { "pending": [ - { - "id": "b9598089-3874-4574-9b1f-d31669eab645", - "serie_id": "beheneko-the-elf-girls-cat-is-secretly-an-s-ranked-monster", - "serie_folder": "beheneko the elf girls cat is secretly an s ranked monster (2025) (2025)", - "serie_name": "beheneko the elf girls cat is secretly an s ranked monster (2025) (2025)", - "episode": { - "season": 1, - "episode": 2, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-01T17:20:49.339563Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "b7626704-068f-4084-80b6-6f213c121412", - "serie_id": "beheneko-the-elf-girls-cat-is-secretly-an-s-ranked-monster", - "serie_folder": "beheneko the elf girls cat is secretly an s ranked monster (2025) (2025)", - "serie_name": "beheneko the elf girls cat is secretly an s ranked monster (2025) (2025)", - "episode": { - "season": 1, - "episode": 3, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-01T17:20:49.339620Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, { "id": "67d98e0a-ce51-4768-854d-33c565d9bf17", "serie_id": "beheneko-the-elf-girls-cat-is-secretly-an-s-ranked-monster", @@ -63,5 +23,5 @@ ], "active": [], "failed": [], - "timestamp": "2025-11-01T17:22:28.998624+00:00" + "timestamp": "2025-11-01T17:42:05.983160+00:00" } \ No newline at end of file diff --git a/src/core/SeriesApp.py b/src/core/SeriesApp.py index a9c0fb9..52ff38e 100644 --- a/src/core/SeriesApp.py +++ b/src/core/SeriesApp.py @@ -245,6 +245,8 @@ class SeriesApp: # event-driven progress reporting with the legacy callback API that # the CLI still relies on. def wrapped_callback(progress_info): + logger.debug(f"wrapped_callback called with: {progress_info}") + if self._is_cancelled(): raise InterruptedError("Download cancelled by user") @@ -267,6 +269,11 @@ class SeriesApp: speed = progress_info.get('speed', 0) # bytes/sec eta = progress_info.get('eta') # seconds + logger.debug( + f"Progress calculation: {downloaded}/{total_bytes} = " + f"{progress:.1f}%, speed={speed}, eta={eta}" + ) + # Convert to expected format for web API callback # Web API expects: percent, downloaded_mb, total_mb, # speed_mbps, eta_seconds @@ -295,6 +302,7 @@ class SeriesApp: 'speed_mbps': None, 'eta_seconds': None, } + logger.debug(f"Old-style progress: {progress}%") # Notify progress via new callback system self._callback_manager.notify_progress( @@ -317,6 +325,7 @@ class SeriesApp: # Call callback with web API format # (dict with detailed progress info) if callback: + logger.debug(f"Calling progress callback: {web_progress_dict}") callback(web_progress_dict) # Propagate progress into the legacy callback chain so diff --git a/src/core/providers/aniworld_provider.py b/src/core/providers/aniworld_provider.py index d4bd769..18a0c6a 100644 --- a/src/core/providers/aniworld_provider.py +++ b/src/core/providers/aniworld_provider.py @@ -261,26 +261,59 @@ class AniworldLoader(Loader): ydl_opts['http_headers'] = header logging.debug("Using custom headers for download") if progress_callback: - ydl_opts['progress_hooks'] = [progress_callback] + # Wrap the callback to add logging + def logged_progress_callback(d): + logging.debug( + f"YT-DLP progress: status={d.get('status')}, " + f"downloaded={d.get('downloaded_bytes')}, " + f"total={d.get('total_bytes')}, " + f"speed={d.get('speed')}" + ) + progress_callback(d) + + ydl_opts['progress_hooks'] = [logged_progress_callback] + logging.debug("Progress callback registered with YT-DLP") try: logging.debug("Starting YoutubeDL download") + logging.debug(f"Download link: {link[:100]}...") + logging.debug(f"YDL options: {ydl_opts}") + with YoutubeDL(ydl_opts) as ydl: - ydl.download([link]) + info = ydl.extract_info(link, download=True) + logging.debug( + f"Download info: " + f"title={info.get('title')}, " + f"filesize={info.get('filesize')}" + ) if os.path.exists(temp_path): logging.debug("Moving file from temp to final destination") shutil.copy(temp_path, output_path) os.remove(temp_path) - logging.info(f"Download completed successfully: {output_file}") + logging.info( + f"Download completed successfully: {output_file}" + ) self.clear_cache() return True else: - logging.error(f"Download failed: temp file not found at {temp_path}") + logging.error( + f"Download failed: temp file not found at {temp_path}" + ) self.clear_cache() return False + except BrokenPipeError as e: + logging.error( + f"Broken pipe error with provider {provider}: {e}. " + f"This usually means the stream connection was closed." + ) + # Try next provider if available + continue except Exception as e: - logging.error(f"YoutubeDL download failed: {e}") + logging.error( + f"YoutubeDL download failed with provider {provider}: " + f"{type(e).__name__}: {e}" + ) # Try next provider if available continue break diff --git a/src/server/services/download_service.py b/src/server/services/download_service.py index e850c3f..ee716d3 100644 --- a/src/server/services/download_service.py +++ b/src/server/services/download_service.py @@ -701,6 +701,10 @@ class DownloadService: def progress_callback(progress_data: dict) -> None: """Update progress and broadcast to clients.""" try: + logger.debug( + f"Progress callback received: {progress_data}" + ) + # Update item progress item.progress = DownloadProgress( percent=progress_data.get("percent", 0.0), @@ -710,41 +714,73 @@ class DownloadService: eta_seconds=progress_data.get("eta_seconds"), ) + logger.debug( + f"Updated item progress: percent={item.progress.percent:.1f}%, " + f"downloaded={item.progress.downloaded_mb:.1f}MB, " + f"total={item.progress.total_mb}MB, " + f"speed={item.progress.speed_mbps}MB/s" + ) + # Track speed for statistics if item.progress.speed_mbps: self._download_speeds.append(item.progress.speed_mbps) # Update progress service + # Schedule coroutines in a thread-safe manner + # (callback may be called from executor thread) if item.progress.total_mb and item.progress.total_mb > 0: current_mb = int(item.progress.downloaded_mb) total_mb = int(item.progress.total_mb) - asyncio.create_task( - self._progress_service.update_progress( - progress_id=f"download_{item.id}", - current=current_mb, - total=total_mb, - metadata={ - "speed_mbps": item.progress.speed_mbps, - "eta_seconds": item.progress.eta_seconds, - }, - ) + logger.debug( + f"Updating progress service: current={current_mb}MB, " + f"total={total_mb}MB" ) + + try: + loop = asyncio.get_event_loop() + asyncio.run_coroutine_threadsafe( + self._progress_service.update_progress( + progress_id=f"download_{item.id}", + current=current_mb, + total=total_mb, + metadata={ + "speed_mbps": item.progress.speed_mbps, + "eta_seconds": item.progress.eta_seconds, + }, + ), + loop + ) + except RuntimeError as e: + logger.warning( + f"Could not schedule progress update: {e}" + ) # Broadcast update (fire and forget) - asyncio.create_task( - self._broadcast_update( - "download_progress", - { - "download_id": item.id, - "item_id": item.id, - "serie_name": item.serie_name, - "season": item.episode.season, - "episode": item.episode.episode, - "progress": item.progress.model_dump(mode="json"), - }, - ) + logger.debug( + f"Broadcasting download_progress event for item {item.id}" ) + + try: + loop = asyncio.get_event_loop() + asyncio.run_coroutine_threadsafe( + self._broadcast_update( + "download_progress", + { + "download_id": item.id, + "item_id": item.id, + "serie_name": item.serie_name, + "season": item.episode.season, + "episode": item.episode.episode, + "progress": item.progress.model_dump(mode="json"), + }, + ), + loop + ) + except RuntimeError as e: + logger.warning( + f"Could not schedule broadcast: {e}" + ) except Exception as e: logger.error("Progress callback error", error=str(e))