fix mocks

This commit is contained in:
Jesse Bannon 2023-03-13 14:11:39 -07:00
parent 6a3e9cddc0
commit dd11e293f4
8 changed files with 216 additions and 205 deletions

View file

@ -20,8 +20,7 @@ from ytdl_sub.downloaders.generic.validators import MultiUrlValidator
from ytdl_sub.downloaders.generic.validators import UrlThumbnailListValidator from ytdl_sub.downloaders.generic.validators import UrlThumbnailListValidator
from ytdl_sub.downloaders.generic.validators import UrlValidator from ytdl_sub.downloaders.generic.validators import UrlValidator
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
from ytdl_sub.downloaders.ytdlp import extract_info_via_info_json from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.downloaders.ytdlp import extract_info_with_retry
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.entry_parent import EntryParent from ytdl_sub.entries.entry_parent import EntryParent
from ytdl_sub.entries.variables.kwargs import COMMENTS from ytdl_sub.entries.variables.kwargs import COMMENTS
@ -266,7 +265,7 @@ class YtDlpDownloader(BaseDownloader[DownloaderOptionsT], ABC):
FileHandler.delete(info_json_file) FileHandler.delete(info_json_file)
def _extract_entry_info_with_retry(self, entry: Entry) -> Entry: def _extract_entry_info_with_retry(self, entry: Entry) -> Entry:
download_entry_dict = extract_info_with_retry( download_entry_dict = YTDLP.extract_info_with_retry(
ytdl_options_overrides=self.download_ytdl_options, ytdl_options_overrides=self.download_ytdl_options,
is_downloaded_fn=None if self.is_dry_run else entry.is_downloaded, is_downloaded_fn=None if self.is_dry_run else entry.is_downloaded,
is_thumbnail_downloaded_fn=None is_thumbnail_downloaded_fn=None
@ -334,7 +333,7 @@ class YtDlpDownloader(BaseDownloader[DownloaderOptionsT], ABC):
url = self.overrides.apply_formatter(collection_url.url) url = self.overrides.apply_formatter(collection_url.url)
with self._separate_download_archives(): with self._separate_download_archives():
entry_dicts = extract_info_via_info_json( entry_dicts = YTDLP.extract_info_via_info_json(
working_directory=self.working_directory, working_directory=self.working_directory,
ytdl_options_overrides=self.metadata_ytdl_options, ytdl_options_overrides=self.metadata_ytdl_options,
log_prefix_on_info_json_dl="Downloading metadata for", log_prefix_on_info_json_dl="Downloading metadata for",

View file

@ -19,25 +19,27 @@ from ytdl_sub.thread.log_entries_downloaded_listener import LogEntriesDownloaded
from ytdl_sub.utils.exceptions import FileNotDownloadedException from ytdl_sub.utils.exceptions import FileNotDownloadedException
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
download_logger = Logger.get(name="yt-dlp-downloader")
_extract_entry_num_retries: int = 5 class YTDLP:
_extract_entry_retry_wait_sec: int = 5 _EXTRACT_ENTRY_NUM_RETRIES: int = 5
_EXTRACT_ENTRY_RETRY_WAIT_SEC: int = 5
logger = Logger.get(name="yt-dlp-downloader")
@contextmanager @classmethod
def ytdlp_downloader(ytdl_options_overrides: Dict) -> ytdl.YoutubeDL: @contextmanager
def ytdlp_downloader(cls, ytdl_options_overrides: Dict) -> ytdl.YoutubeDL:
""" """
Context manager to interact with yt_dlp. Context manager to interact with yt_dlp.
""" """
download_logger.debug("ytdl_options: %s", str(ytdl_options_overrides)) cls.logger.debug("ytdl_options: %s", str(ytdl_options_overrides))
with Logger.handle_external_logs(name="yt-dlp"): with Logger.handle_external_logs(name="yt-dlp"):
# Deep copy ytdl_options in case yt-dlp modifies the dict # Deep copy ytdl_options in case yt-dlp modifies the dict
with ytdl.YoutubeDL(copy.deepcopy(ytdl_options_overrides)) as ytdl_downloader: with ytdl.YoutubeDL(copy.deepcopy(ytdl_options_overrides)) as ytdl_downloader:
yield ytdl_downloader yield ytdl_downloader
@classmethod
def extract_info(ytdl_options_overrides: Dict, **kwargs) -> Dict: def extract_info(cls, ytdl_options_overrides: Dict, **kwargs) -> Dict:
""" """
Wrapper around yt_dlp.YoutubeDL.YoutubeDL.extract_info Wrapper around yt_dlp.YoutubeDL.YoutubeDL.extract_info
All kwargs will passed to the extract_info function. All kwargs will passed to the extract_info function.
@ -49,16 +51,17 @@ def extract_info(ytdl_options_overrides: Dict, **kwargs) -> Dict:
**kwargs **kwargs
arguments passed directory to YoutubeDL extract_info arguments passed directory to YoutubeDL extract_info
""" """
with ytdlp_downloader(ytdl_options_overrides) as ytdlp: with cls.ytdlp_downloader(ytdl_options_overrides) as ytdlp:
return ytdlp.extract_info(**kwargs) return ytdlp.extract_info(**kwargs)
@classmethod
def extract_info_with_retry( def extract_info_with_retry(
cls,
ytdl_options_overrides: Dict, ytdl_options_overrides: Dict,
is_downloaded_fn: Optional[Callable[[], bool]] = None, is_downloaded_fn: Optional[Callable[[], bool]] = None,
is_thumbnail_downloaded_fn: Optional[Callable[[], bool]] = None, is_thumbnail_downloaded_fn: Optional[Callable[[], bool]] = None,
**kwargs, **kwargs,
) -> Dict: ) -> Dict:
""" """
Wrapper around yt_dlp.YoutubeDL.YoutubeDL.extract_info Wrapper around yt_dlp.YoutubeDL.YoutubeDL.extract_info
All kwargs will passed to the extract_info function. All kwargs will passed to the extract_info function.
@ -86,11 +89,15 @@ def extract_info_with_retry(
entry_files_exist = False entry_files_exist = False
copied_ytdl_options_overrides = copy.deepcopy(ytdl_options_overrides) copied_ytdl_options_overrides = copy.deepcopy(ytdl_options_overrides)
while not entry_files_exist and num_tries < _extract_entry_num_retries: while not entry_files_exist and num_tries < cls._EXTRACT_ENTRY_NUM_RETRIES:
entry_dict = extract_info(ytdl_options_overrides=copied_ytdl_options_overrides, **kwargs) entry_dict = cls.extract_info(
ytdl_options_overrides=copied_ytdl_options_overrides, **kwargs
)
is_downloaded = is_downloaded_fn is None or is_downloaded_fn() is_downloaded = is_downloaded_fn is None or is_downloaded_fn()
is_thumbnail_downloaded = is_thumbnail_downloaded_fn is None or is_thumbnail_downloaded_fn() is_thumbnail_downloaded = (
is_thumbnail_downloaded_fn is None or is_thumbnail_downloaded_fn()
)
if is_downloaded and is_thumbnail_downloaded: if is_downloaded and is_thumbnail_downloaded:
return entry_dict return entry_dict
@ -101,7 +108,7 @@ def extract_info_with_retry(
copied_ytdl_options_overrides["skip_download"] = True copied_ytdl_options_overrides["skip_download"] = True
copied_ytdl_options_overrides["writethumbnail"] = True copied_ytdl_options_overrides["writethumbnail"] = True
time.sleep(_extract_entry_retry_wait_sec) time.sleep(cls._EXTRACT_ENTRY_RETRY_WAIT_SEC)
num_tries += 1 num_tries += 1
# Remove the download archive so it can retry without thinking its already downloaded, # Remove the download archive so it can retry without thinking its already downloaded,
@ -109,11 +116,11 @@ def extract_info_with_retry(
if "download_archive" in copied_ytdl_options_overrides: if "download_archive" in copied_ytdl_options_overrides:
del copied_ytdl_options_overrides["download_archive"] del copied_ytdl_options_overrides["download_archive"]
if num_tries < _extract_entry_num_retries: if num_tries < cls._EXTRACT_ENTRY_NUM_RETRIES:
download_logger.debug( cls.logger.debug(
"Failed to download entry. Retrying %d / %d", "Failed to download entry. Retrying %d / %d",
num_tries, num_tries,
_extract_entry_num_retries, cls._EXTRACT_ENTRY_NUM_RETRIES,
) )
error_dict = {"ytdl_options": ytdl_options_overrides, "kwargs": kwargs} error_dict = {"ytdl_options": ytdl_options_overrides, "kwargs": kwargs}
@ -121,8 +128,8 @@ def extract_info_with_retry(
f"yt-dlp failed to download an entry with these arguments: {error_dict}" f"yt-dlp failed to download an entry with these arguments: {error_dict}"
) )
@classmethod
def _get_entry_dicts_from_info_json_files(working_directory: str) -> List[Dict]: def _get_entry_dicts_from_info_json_files(cls, working_directory: str) -> List[Dict]:
""" """
Parameters Parameters
---------- ----------
@ -146,9 +153,11 @@ def _get_entry_dicts_from_info_json_files(working_directory: str) -> List[Dict]:
return entry_dicts return entry_dicts
@classmethod
@contextlib.contextmanager @contextlib.contextmanager
def _listen_and_log_downloaded_info_json(working_directory: str, log_prefix: Optional[str]): def _listen_and_log_downloaded_info_json(
cls, working_directory: str, log_prefix: Optional[str]
):
""" """
Context manager that starts a separate thread that listens for new .info.json files, Context manager that starts a separate thread that listens for new .info.json files,
prints their titles as they appear prints their titles as they appear
@ -169,13 +178,14 @@ def _listen_and_log_downloaded_info_json(working_directory: str, log_prefix: Opt
finally: finally:
info_json_listener.complete = True info_json_listener.complete = True
@classmethod
def extract_info_via_info_json( def extract_info_via_info_json(
cls,
working_directory: str, working_directory: str,
ytdl_options_overrides: Dict, ytdl_options_overrides: Dict,
log_prefix_on_info_json_dl: Optional[str] = None, log_prefix_on_info_json_dl: Optional[str] = None,
**kwargs, **kwargs,
) -> List[Dict]: ) -> List[Dict]:
""" """
Wrapper around yt_dlp.YoutubeDL.YoutubeDL.extract_info with infojson enabled. Entry dicts Wrapper around yt_dlp.YoutubeDL.YoutubeDL.extract_info with infojson enabled. Entry dicts
are extracted via reading all info.json files in the working directory rather than are extracted via reading all info.json files in the working directory rather than
@ -197,21 +207,21 @@ def extract_info_via_info_json(
arguments passed directory to YoutubeDL extract_info arguments passed directory to YoutubeDL extract_info
""" """
try: try:
with _listen_and_log_downloaded_info_json( with cls._listen_and_log_downloaded_info_json(
working_directory=working_directory, log_prefix=log_prefix_on_info_json_dl working_directory=working_directory, log_prefix=log_prefix_on_info_json_dl
): ):
_ = extract_info(ytdl_options_overrides=ytdl_options_overrides, **kwargs) _ = cls.extract_info(ytdl_options_overrides=ytdl_options_overrides, **kwargs)
except RejectedVideoReached: except RejectedVideoReached:
download_logger.debug( cls.logger.debug(
"RejectedVideoReached, stopping additional downloads " "RejectedVideoReached, stopping additional downloads "
"(Can be disable by setting `ytdl_options.break_on_reject` to False)." "(Can be disable by setting `ytdl_options.break_on_reject` to False)."
) )
except ExistingVideoReached: except ExistingVideoReached:
download_logger.debug( cls.logger.debug(
"ExistingVideoReached, stopping additional downloads. " "ExistingVideoReached, stopping additional downloads. "
"(Can be disable by setting `ytdl_options.break_on_existing` to False)." "(Can be disable by setting `ytdl_options.break_on_existing` to False)."
) )
except MaxDownloadsReached: except MaxDownloadsReached:
download_logger.info("MaxDownloadsReached, stopping additional downloads.") cls.logger.info("MaxDownloadsReached, stopping additional downloads.")
return _get_entry_dicts_from_info_json_files(working_directory=working_directory) return cls._get_entry_dicts_from_info_json_files(working_directory=working_directory)

View file

@ -4,6 +4,7 @@ from expected_download import assert_expected_downloads
from expected_transaction_log import assert_transaction_log_matches from expected_transaction_log import assert_transaction_log_matches
import ytdl_sub.downloaders.downloader import ytdl_sub.downloaders.downloader
from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.subscriptions.subscription import Subscription
@ -65,7 +66,7 @@ class TestBandcamp:
# Ensure another invocation will hit ExistingVideoReached # Ensure another invocation will hit ExistingVideoReached
if not dry_run: if not dry_run:
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="ExistingVideoReached, stopping additional downloads", expected_message="ExistingVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):

View file

@ -7,6 +7,7 @@ from expected_download import assert_expected_downloads
from expected_transaction_log import assert_transaction_log_matches from expected_transaction_log import assert_transaction_log_matches
import ytdl_sub.downloaders.downloader import ytdl_sub.downloaders.downloader
from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.subscriptions.subscription import Subscription
@ -68,7 +69,7 @@ class TestDateRange:
if not dry_run: if not dry_run:
# try downloading again, ensure nothing more was downloaded # try downloading again, ensure nothing more was downloaded
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="ExistingVideoReached, stopping additional downloads", expected_message="ExistingVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):
@ -139,7 +140,7 @@ class TestDateRange:
# First, download recent vids. Always download since we want to test dry-run # First, download recent vids. Always download since we want to test dry-run
# on the rolling recent portion. # on the rolling recent portion.
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="RejectedVideoReached, stopping additional downloads", expected_message="RejectedVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):
@ -159,7 +160,7 @@ class TestDateRange:
# Then, download the rolling recent vids subscription. This should remove one of the # Then, download the rolling recent vids subscription. This should remove one of the
# two videos # two videos
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="ExistingVideoReached, stopping additional downloads", expected_message="ExistingVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):
@ -186,7 +187,7 @@ class TestDateRange:
# existing # existing
if not dry_run: if not dry_run:
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="ExistingVideoReached, stopping additional downloads", expected_message="ExistingVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):

View file

@ -5,6 +5,7 @@ from expected_download import assert_expected_downloads
from expected_transaction_log import assert_transaction_log_matches from expected_transaction_log import assert_transaction_log_matches
import ytdl_sub.downloaders.downloader import ytdl_sub.downloaders.downloader
from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.subscriptions.subscription import Subscription
@ -83,7 +84,7 @@ class TestPlaylist:
# Ensure another invocation will hit ExistingVideoReached # Ensure another invocation will hit ExistingVideoReached
if not dry_run: if not dry_run:
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="ExistingVideoReached, stopping additional downloads", expected_message="ExistingVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):
@ -131,7 +132,7 @@ class TestPlaylist:
if not dry_run: if not dry_run:
# Ensure another invocation will hit ExistingVideoReached # Ensure another invocation will hit ExistingVideoReached
with assert_logs( with assert_logs(
logger=ytdl_sub.downloaders.downloader.download_logger, logger=YTDLP.logger,
expected_message="ExistingVideoReached, stopping additional downloads", expected_message="ExistingVideoReached, stopping additional downloads",
log_level="debug", log_level="debug",
): ):

View file

@ -1,6 +1,5 @@
import json import json
import os.path import os.path
import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List from typing import List

View file

@ -12,6 +12,7 @@ from resources import copy_file_fixture
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.downloaders.downloader import YtDlpDownloader from ytdl_sub.downloaders.downloader import YtDlpDownloader
from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.entries.variables.kwargs import DESCRIPTION from ytdl_sub.entries.variables.kwargs import DESCRIPTION
from ytdl_sub.entries.variables.kwargs import EPOCH from ytdl_sub.entries.variables.kwargs import EPOCH
from ytdl_sub.entries.variables.kwargs import EXT from ytdl_sub.entries.variables.kwargs import EXT
@ -100,23 +101,22 @@ def mock_entry_dict_factory(mock_downloaded_file_path) -> Callable:
@pytest.fixture @pytest.fixture
def mock_download_collection_thumbnail(mock_downloaded_file_path): def mock_download_collection_thumbnail(mock_downloaded_file_path):
def _mock_download_thumbnail(output_path: str) -> bool: def _mock_download_and_convert_url_thumbnail(
# mock_file_factory(file_name=output_path.split("/")[-1]) thumbnail_url: str, output_thumbnail_path: str
output_name = os.path.basename(output_path) ) -> bool:
_ = thumbnail_url
output_name = os.path.basename(output_thumbnail_path)
if "poster" in output_name or "show" in output_name: if "poster" in output_name or "show" in output_name:
copy_file_fixture(fixture_name="poster.jpg", output_file_path=output_path) copy_file_fixture(fixture_name="poster.jpg", output_file_path=output_thumbnail_path)
return True return True
elif "fanart" in output_name: elif "fanart" in output_name:
copy_file_fixture(fixture_name="fanart.jpeg", output_file_path=output_path) copy_file_fixture(fixture_name="fanart.jpeg", output_file_path=output_thumbnail_path)
return True return True
return False return False
with patch.object( with patch(
YtDlpDownloader, "ytdl_sub.downloaders.downloader.download_and_convert_url_thumbnail",
"_download_thumbnail", new=_mock_download_and_convert_url_thumbnail,
new=lambda _, thumbnail_url, output_thumbnail_path: _mock_download_thumbnail(
output_thumbnail_path
),
): ):
yield # TODO: create file here yield # TODO: create file here
@ -126,11 +126,9 @@ def mock_download_collection_entries(
mock_download_collection_thumbnail, mock_entry_dict_factory: Callable, working_directory: str mock_download_collection_thumbnail, mock_entry_dict_factory: Callable, working_directory: str
): ):
@contextlib.contextmanager @contextlib.contextmanager
def _mock_download_collection_entries_factory(is_youtube_channel: bool): def _mock_download_collection_entries_factory(is_youtube_channel: bool, num_urls: int = 1):
def _write_entries_to_working_dir(*args, **kwargs) -> List[Dict]: def _write_entries_to_working_dir(*args, **kwargs) -> List[Dict]:
if (len(args[0].collection.urls.list) == 1) or ( if num_urls == 1 or ("season.2" in kwargs["url"] and num_urls > 1):
"season.2" in kwargs["url"] and len(args[0].download_options.urls.list) > 1
):
return [ return [
mock_entry_dict_factory( mock_entry_dict_factory(
uid="21-1", uid="21-1",
@ -202,7 +200,7 @@ def mock_download_collection_entries(
] ]
with patch.object( with patch.object(
YtDlpDownloader, "extract_info_via_info_json", new=_write_entries_to_working_dir YTDLP, "extract_info_via_info_json", new=_write_entries_to_working_dir
), patch.object( ), patch.object(
YtDlpDownloader, "_extract_entry_info_with_retry", new=lambda _, entry: entry YtDlpDownloader, "_extract_entry_info_with_retry", new=lambda _, entry: entry
): ):

View file

@ -215,7 +215,9 @@ class TestPrebuiltTvShowCollectionPresets:
}, },
) )
with mock_download_collection_entries(is_youtube_channel=is_youtube_channel): with mock_download_collection_entries(
is_youtube_channel=is_youtube_channel, num_urls=len(season_indices)
):
transaction_log = subscription.download(dry_run=False) transaction_log = subscription.download(dry_run=False)
assert_transaction_log_matches( assert_transaction_log_matches(