[BUGFIX] Retry channel image downloads, continue if it fails (#218)
This commit is contained in:
parent
3e801c56eb
commit
6383d133dc
4 changed files with 98 additions and 10 deletions
|
|
@ -228,7 +228,7 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions
|
|||
self,
|
||||
thumbnail_url: str,
|
||||
output_thumbnail_path: str,
|
||||
):
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
Downloads a thumbnail and stores it in the output directory
|
||||
|
||||
|
|
@ -238,12 +238,16 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions
|
|||
Url of the thumbnail
|
||||
output_thumbnail_path:
|
||||
Path to store the thumbnail after downloading
|
||||
|
||||
Returns
|
||||
-------
|
||||
True if the thumbnail converted. None if it is missing or failed.
|
||||
"""
|
||||
if not thumbnail_url:
|
||||
download_logger.warning("Could not find a thumbnail for %s", self.channel.uid)
|
||||
return
|
||||
return None
|
||||
|
||||
convert_url_thumbnail(
|
||||
return convert_url_thumbnail(
|
||||
thumbnail_url=thumbnail_url, output_thumbnail_path=output_thumbnail_path
|
||||
)
|
||||
|
||||
|
|
@ -255,18 +259,22 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions
|
|||
avatar_thumbnail_name = self.overrides.apply_formatter(
|
||||
self.download_options.channel_avatar_path
|
||||
)
|
||||
self._download_thumbnail(
|
||||
if self._download_thumbnail(
|
||||
thumbnail_url=self.channel.avatar_thumbnail_url(),
|
||||
output_thumbnail_path=str(Path(self.working_directory) / avatar_thumbnail_name),
|
||||
)
|
||||
self.save_file(file_name=avatar_thumbnail_name)
|
||||
):
|
||||
self.save_file(file_name=avatar_thumbnail_name)
|
||||
else:
|
||||
download_logger.warning("Failed to download channel's avatar image")
|
||||
|
||||
if self.download_options.channel_banner_path:
|
||||
banner_thumbnail_name = self.overrides.apply_formatter(
|
||||
self.download_options.channel_banner_path
|
||||
)
|
||||
self._download_thumbnail(
|
||||
if self._download_thumbnail(
|
||||
thumbnail_url=self.channel.banner_thumbnail_url(),
|
||||
output_thumbnail_path=str(Path(self.working_directory) / banner_thumbnail_name),
|
||||
)
|
||||
self.save_file(file_name=banner_thumbnail_name)
|
||||
):
|
||||
self.save_file(file_name=banner_thumbnail_name)
|
||||
else:
|
||||
download_logger.warning("Failed to download channel's banner image")
|
||||
|
|
|
|||
50
src/ytdl_sub/utils/retry.py
Normal file
50
src/ytdl_sub/utils/retry.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from time import sleep
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
|
||||
logger = Logger.get()
|
||||
|
||||
|
||||
def retry(times: int, exceptions: Tuple[Type[Exception], ...], wait_sec: int = 5) -> Optional[Any]:
|
||||
"""
|
||||
Retry decorator
|
||||
|
||||
Parameters
|
||||
----------
|
||||
times
|
||||
Number of attempts to retry
|
||||
exceptions
|
||||
Type of exceptions to retry against
|
||||
wait_sec
|
||||
Number of seconds to wait inbetween retries
|
||||
|
||||
Returns
|
||||
-------
|
||||
Any or None
|
||||
If none, it implies all retries failed
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
def newfn(*args, **kwargs):
|
||||
attempt = 0
|
||||
while attempt < times:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except exceptions:
|
||||
logger.debug(
|
||||
"Exception thrown when attempting to run %s, attempt %d of %d",
|
||||
func.__name__,
|
||||
attempt + 1,
|
||||
times,
|
||||
)
|
||||
attempt += 1
|
||||
sleep(wait_sec)
|
||||
return None
|
||||
|
||||
return newfn
|
||||
|
||||
return decorator
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import tempfile
|
||||
from typing import Optional
|
||||
from urllib.request import urlopen
|
||||
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||
from ytdl_sub.utils.retry import retry
|
||||
|
||||
|
||||
def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None:
|
||||
|
|
@ -32,7 +34,8 @@ def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) ->
|
|||
FFMPEG.run(["-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg])
|
||||
|
||||
|
||||
def convert_url_thumbnail(thumbnail_url: str, output_thumbnail_path: str):
|
||||
@retry(times=5, exceptions=(Exception,))
|
||||
def convert_url_thumbnail(thumbnail_url: str, output_thumbnail_path: str) -> Optional[bool]:
|
||||
"""
|
||||
Downloads and converts a thumbnail from a url into a jpg
|
||||
|
||||
|
|
@ -42,9 +45,15 @@ def convert_url_thumbnail(thumbnail_url: str, output_thumbnail_path: str):
|
|||
URL of the thumbnail
|
||||
output_thumbnail_path
|
||||
Thumbnail file destination after its converted to jpg
|
||||
|
||||
Returns
|
||||
-------
|
||||
True to indicate it converted the thumbnail from url. None if the retry failed.
|
||||
"""
|
||||
with urlopen(thumbnail_url) as file:
|
||||
with tempfile.NamedTemporaryFile() as thumbnail:
|
||||
thumbnail.write(file.read())
|
||||
|
||||
FFMPEG.run(["-bitexact", "-i", thumbnail.name, output_thumbnail_path, "-bitexact"])
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from conftest import assert_debug_log
|
||||
from e2e.expected_download import assert_expected_downloads
|
||||
from e2e.expected_transaction_log import assert_transaction_log_matches
|
||||
|
||||
from ytdl_sub.subscriptions.subscription import Subscription
|
||||
from ytdl_sub.utils.retry import logger as retry_logger
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -59,3 +63,20 @@ class TestChannelAsKodiTvShow:
|
|||
dry_run=dry_run,
|
||||
expected_download_summary_file_name="youtube/test_channel_full.json",
|
||||
)
|
||||
|
||||
def test_channel_post_download(self, channel_as_tv_show_config, channel_preset_dict):
|
||||
channel_preset_dict["ytdl_options"]["max_views"] = 1 # no downloads occur
|
||||
full_channel_subscription = Subscription.from_dict(
|
||||
config=channel_as_tv_show_config, preset_name="pz", preset_dict=channel_preset_dict
|
||||
)
|
||||
|
||||
with assert_debug_log( # Ensure retry debug message is thrown
|
||||
logger=retry_logger,
|
||||
expected_message="Exception thrown when attempting to run %s, attempt %d of %d",
|
||||
), patch( # Make sleeps instant
|
||||
"ytdl_sub.utils.retry.sleep"
|
||||
), patch( # Mock error when calling urlopen
|
||||
"ytdl_sub.utils.thumbnail.urlopen"
|
||||
) as mock_urlopen:
|
||||
mock_urlopen.side_effect = [Exception("error")]
|
||||
full_channel_subscription.download(dry_run=True)
|
||||
|
|
|
|||
Loading…
Reference in a new issue