Fix: Use yt_dlp.utils.DownloadCancelled for proper download cancellation

- Import and use DownloadCancelled exception which YT-DLP properly handles
- Add InterruptedError handling throughout the call chain
- Fire 'cancelled' status event when download is cancelled
- Handle InterruptedError in DownloadService to set CANCELLED status
This commit is contained in:
Lukas 2025-12-27 19:38:12 +01:00
parent 08f816a954
commit 4780f68a23
4 changed files with 74 additions and 9 deletions

View File

@ -419,6 +419,29 @@ class SeriesApp:
return download_success
except InterruptedError:
# Download was cancelled - propagate the cancellation
logger.info(
"Download cancelled: %s (key: %s) S%02dE%02d",
serie_folder,
key,
season,
episode,
)
# Fire download cancelled event
self._events.download_status(
DownloadStatusEventArgs(
serie_folder=serie_folder,
key=key,
season=season,
episode=episode,
status="cancelled",
message="Download cancelled by user",
item_id=item_id,
)
)
raise # Re-raise to propagate cancellation
except Exception as e: # pylint: disable=broad-except
logger.error(
"Download error: %s (key: %s) S%02dE%02d - %s",

View File

@ -4,6 +4,8 @@ import logging
import os
import re
import shutil
import signal
import sys
import threading
from pathlib import Path
from urllib.parse import quote
@ -14,6 +16,7 @@ from fake_useragent import UserAgent
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from yt_dlp import YoutubeDL
from yt_dlp.utils import DownloadCancelled
from ..interfaces.providers import Providers
from .base_provider import Loader
@ -308,14 +311,19 @@ class AniworldLoader(Loader):
)
logging.debug("Direct link obtained from provider")
# Create a cancellation-aware progress hook
# Create a cancellation-aware progress hook using DownloadCancelled
# which YT-DLP properly handles
cancel_flag = self._cancel_flag
def cancellation_check_hook(d):
"""Progress hook that checks for cancellation."""
"""Progress hook that checks for cancellation.
Uses yt_dlp.utils.DownloadCancelled which is properly
handled by YT-DLP to abort downloads immediately.
"""
if cancel_flag.is_set():
logging.info("Cancellation detected in progress hook")
raise InterruptedError("Download cancelled")
raise DownloadCancelled("Download cancelled by user")
ydl_opts = {
'fragment_retries': float('inf'),
@ -334,10 +342,10 @@ class AniworldLoader(Loader):
if progress_callback:
# Wrap the callback to add logging and keep cancellation check
def logged_progress_callback(d):
# Check cancellation first
# Check cancellation first - use DownloadCancelled
if cancel_flag.is_set():
logging.info("Cancellation detected in progress callback")
raise InterruptedError("Download cancelled")
raise DownloadCancelled("Download cancelled by user")
logging.debug(
f"YT-DLP progress: status={d.get('status')}, "
f"downloaded={d.get('downloaded_bytes')}, "
@ -385,16 +393,19 @@ class AniworldLoader(Loader):
)
self.clear_cache()
return False
except InterruptedError:
except (InterruptedError, DownloadCancelled) as e:
# Re-raise cancellation errors
logging.info("Download interrupted, propagating cancellation")
logging.info(
"Download cancelled: %s, propagating cancellation",
type(e).__name__
)
# Clean up temp file if exists
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
raise
raise InterruptedError("Download cancelled") from e
except BrokenPipeError as e:
logging.error(
f"Broken pipe error with provider {provider}: {e}. "
@ -403,6 +414,15 @@ class AniworldLoader(Loader):
# Try next provider if available
continue
except Exception as e:
# Check if this is a cancellation wrapped in another exception
if self.is_cancelled():
logging.info("Download cancelled (detected in exception handler)")
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
raise InterruptedError("Download cancelled") from e
logging.error(
f"YoutubeDL download failed with provider {provider}: "
f"{type(e).__name__}: {e}"

View File

@ -846,6 +846,7 @@ class AnimeService:
Raises:
AnimeServiceError: If download fails
InterruptedError: If download was cancelled
Note:
The 'key' parameter is the primary identifier used for all
@ -864,6 +865,10 @@ class AnimeService:
key=key,
item_id=item_id,
)
except InterruptedError:
# Download was cancelled - re-raise for proper handling
logger.info("Download cancelled, propagating cancellation")
raise
except Exception as exc:
logger.exception("download failed")
raise AnimeServiceError("Download failed") from exc

View File

@ -952,7 +952,7 @@ class DownloadService:
except asyncio.CancelledError:
# Handle task cancellation during shutdown
logger.info(
"Download cancelled during shutdown: item_id=%s",
"Download task cancelled: item_id=%s",
item.id,
)
item.status = DownloadStatus.CANCELLED
@ -965,6 +965,23 @@ class DownloadService:
# Re-save to database as pending
await self._save_to_database(item)
raise # Re-raise to properly cancel the task
except InterruptedError:
# Handle download cancellation from provider
logger.info(
"Download interrupted/cancelled: item_id=%s",
item.id,
)
item.status = DownloadStatus.CANCELLED
item.completed_at = datetime.now(timezone.utc)
# Delete cancelled item from database
await self._delete_from_database(item.id)
# Return item to pending queue if not shutting down
if not self._is_shutting_down:
self._add_to_pending_queue(item, front=True)
# Re-save to database as pending
await self._save_to_database(item)
# Don't re-raise - this is handled gracefully
except Exception as e:
# Handle failure