[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,
|
self,
|
||||||
thumbnail_url: str,
|
thumbnail_url: str,
|
||||||
output_thumbnail_path: str,
|
output_thumbnail_path: str,
|
||||||
):
|
) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
Downloads a thumbnail and stores it in the output directory
|
Downloads a thumbnail and stores it in the output directory
|
||||||
|
|
||||||
|
|
@ -238,12 +238,16 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions
|
||||||
Url of the thumbnail
|
Url of the thumbnail
|
||||||
output_thumbnail_path:
|
output_thumbnail_path:
|
||||||
Path to store the thumbnail after downloading
|
Path to store the thumbnail after downloading
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
True if the thumbnail converted. None if it is missing or failed.
|
||||||
"""
|
"""
|
||||||
if not thumbnail_url:
|
if not thumbnail_url:
|
||||||
download_logger.warning("Could not find a thumbnail for %s", self.channel.uid)
|
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
|
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(
|
avatar_thumbnail_name = self.overrides.apply_formatter(
|
||||||
self.download_options.channel_avatar_path
|
self.download_options.channel_avatar_path
|
||||||
)
|
)
|
||||||
self._download_thumbnail(
|
if self._download_thumbnail(
|
||||||
thumbnail_url=self.channel.avatar_thumbnail_url(),
|
thumbnail_url=self.channel.avatar_thumbnail_url(),
|
||||||
output_thumbnail_path=str(Path(self.working_directory) / avatar_thumbnail_name),
|
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:
|
if self.download_options.channel_banner_path:
|
||||||
banner_thumbnail_name = self.overrides.apply_formatter(
|
banner_thumbnail_name = self.overrides.apply_formatter(
|
||||||
self.download_options.channel_banner_path
|
self.download_options.channel_banner_path
|
||||||
)
|
)
|
||||||
self._download_thumbnail(
|
if self._download_thumbnail(
|
||||||
thumbnail_url=self.channel.banner_thumbnail_url(),
|
thumbnail_url=self.channel.banner_thumbnail_url(),
|
||||||
output_thumbnail_path=str(Path(self.working_directory) / banner_thumbnail_name),
|
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
|
import tempfile
|
||||||
|
from typing import Optional
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
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:
|
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])
|
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
|
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
|
URL of the thumbnail
|
||||||
output_thumbnail_path
|
output_thumbnail_path
|
||||||
Thumbnail file destination after its converted to jpg
|
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 urlopen(thumbnail_url) as file:
|
||||||
with tempfile.NamedTemporaryFile() as thumbnail:
|
with tempfile.NamedTemporaryFile() as thumbnail:
|
||||||
thumbnail.write(file.read())
|
thumbnail.write(file.read())
|
||||||
|
|
||||||
FFMPEG.run(["-bitexact", "-i", thumbnail.name, output_thumbnail_path, "-bitexact"])
|
FFMPEG.run(["-bitexact", "-i", thumbnail.name, output_thumbnail_path, "-bitexact"])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from conftest import assert_debug_log
|
||||||
from e2e.expected_download import assert_expected_downloads
|
from e2e.expected_download import assert_expected_downloads
|
||||||
from e2e.expected_transaction_log import assert_transaction_log_matches
|
from e2e.expected_transaction_log import assert_transaction_log_matches
|
||||||
|
|
||||||
from ytdl_sub.subscriptions.subscription import Subscription
|
from ytdl_sub.subscriptions.subscription import Subscription
|
||||||
|
from ytdl_sub.utils.retry import logger as retry_logger
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -59,3 +63,20 @@ class TestChannelAsKodiTvShow:
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
expected_download_summary_file_name="youtube/test_channel_full.json",
|
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