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:
parent
08f816a954
commit
4780f68a23
@ -419,6 +419,29 @@ class SeriesApp:
|
|||||||
|
|
||||||
return download_success
|
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
|
except Exception as e: # pylint: disable=broad-except
|
||||||
logger.error(
|
logger.error(
|
||||||
"Download error: %s (key: %s) S%02dE%02d - %s",
|
"Download error: %s (key: %s) S%02dE%02d - %s",
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@ -14,6 +16,7 @@ from fake_useragent import UserAgent
|
|||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.util.retry import Retry
|
from urllib3.util.retry import Retry
|
||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
|
from yt_dlp.utils import DownloadCancelled
|
||||||
|
|
||||||
from ..interfaces.providers import Providers
|
from ..interfaces.providers import Providers
|
||||||
from .base_provider import Loader
|
from .base_provider import Loader
|
||||||
@ -308,14 +311,19 @@ class AniworldLoader(Loader):
|
|||||||
)
|
)
|
||||||
logging.debug("Direct link obtained from provider")
|
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
|
cancel_flag = self._cancel_flag
|
||||||
|
|
||||||
def cancellation_check_hook(d):
|
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():
|
if cancel_flag.is_set():
|
||||||
logging.info("Cancellation detected in progress hook")
|
logging.info("Cancellation detected in progress hook")
|
||||||
raise InterruptedError("Download cancelled")
|
raise DownloadCancelled("Download cancelled by user")
|
||||||
|
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'fragment_retries': float('inf'),
|
'fragment_retries': float('inf'),
|
||||||
@ -334,10 +342,10 @@ class AniworldLoader(Loader):
|
|||||||
if progress_callback:
|
if progress_callback:
|
||||||
# Wrap the callback to add logging and keep cancellation check
|
# Wrap the callback to add logging and keep cancellation check
|
||||||
def logged_progress_callback(d):
|
def logged_progress_callback(d):
|
||||||
# Check cancellation first
|
# Check cancellation first - use DownloadCancelled
|
||||||
if cancel_flag.is_set():
|
if cancel_flag.is_set():
|
||||||
logging.info("Cancellation detected in progress callback")
|
logging.info("Cancellation detected in progress callback")
|
||||||
raise InterruptedError("Download cancelled")
|
raise DownloadCancelled("Download cancelled by user")
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"YT-DLP progress: status={d.get('status')}, "
|
f"YT-DLP progress: status={d.get('status')}, "
|
||||||
f"downloaded={d.get('downloaded_bytes')}, "
|
f"downloaded={d.get('downloaded_bytes')}, "
|
||||||
@ -385,16 +393,19 @@ class AniworldLoader(Loader):
|
|||||||
)
|
)
|
||||||
self.clear_cache()
|
self.clear_cache()
|
||||||
return False
|
return False
|
||||||
except InterruptedError:
|
except (InterruptedError, DownloadCancelled) as e:
|
||||||
# Re-raise cancellation errors
|
# 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
|
# Clean up temp file if exists
|
||||||
if os.path.exists(temp_path):
|
if os.path.exists(temp_path):
|
||||||
try:
|
try:
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
raise
|
raise InterruptedError("Download cancelled") from e
|
||||||
except BrokenPipeError as e:
|
except BrokenPipeError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Broken pipe error with provider {provider}: {e}. "
|
f"Broken pipe error with provider {provider}: {e}. "
|
||||||
@ -403,6 +414,15 @@ class AniworldLoader(Loader):
|
|||||||
# Try next provider if available
|
# Try next provider if available
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
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(
|
logging.error(
|
||||||
f"YoutubeDL download failed with provider {provider}: "
|
f"YoutubeDL download failed with provider {provider}: "
|
||||||
f"{type(e).__name__}: {e}"
|
f"{type(e).__name__}: {e}"
|
||||||
|
|||||||
@ -846,6 +846,7 @@ class AnimeService:
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
AnimeServiceError: If download fails
|
AnimeServiceError: If download fails
|
||||||
|
InterruptedError: If download was cancelled
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
The 'key' parameter is the primary identifier used for all
|
The 'key' parameter is the primary identifier used for all
|
||||||
@ -864,6 +865,10 @@ class AnimeService:
|
|||||||
key=key,
|
key=key,
|
||||||
item_id=item_id,
|
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:
|
except Exception as exc:
|
||||||
logger.exception("download failed")
|
logger.exception("download failed")
|
||||||
raise AnimeServiceError("Download failed") from exc
|
raise AnimeServiceError("Download failed") from exc
|
||||||
|
|||||||
@ -952,7 +952,7 @@ class DownloadService:
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
# Handle task cancellation during shutdown
|
# Handle task cancellation during shutdown
|
||||||
logger.info(
|
logger.info(
|
||||||
"Download cancelled during shutdown: item_id=%s",
|
"Download task cancelled: item_id=%s",
|
||||||
item.id,
|
item.id,
|
||||||
)
|
)
|
||||||
item.status = DownloadStatus.CANCELLED
|
item.status = DownloadStatus.CANCELLED
|
||||||
@ -966,6 +966,23 @@ class DownloadService:
|
|||||||
await self._save_to_database(item)
|
await self._save_to_database(item)
|
||||||
raise # Re-raise to properly cancel the task
|
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:
|
except Exception as e:
|
||||||
# Handle failure
|
# Handle failure
|
||||||
item.status = DownloadStatus.FAILED
|
item.status = DownloadStatus.FAILED
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user