diff --git a/src/ytdl_sub/cli/main.py b/src/ytdl_sub/cli/main.py index 2419c0be..a8491b5e 100644 --- a/src/ytdl_sub/cli/main.py +++ b/src/ytdl_sub/cli/main.py @@ -14,6 +14,7 @@ from ytdl_sub.cli.download_args_parser import DownloadArgsParser from ytdl_sub.cli.main_args_parser import parser from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandlerTransactionLog @@ -62,7 +63,7 @@ def _maybe_write_subscription_log_file( def _download_subscriptions_from_yaml_files( - config: ConfigFile, subscription_paths: List[str], dry_run: bool + config: ConfigFile, subscription_paths: List[str], update_with_info_json: bool, dry_run: bool ) -> List[Tuple[Subscription, FileHandlerTransactionLog]]: """ Downloads all subscriptions from one or many subscription yaml files. @@ -73,6 +74,8 @@ def _download_subscriptions_from_yaml_files( Configuration file subscription_paths Path to subscription files to download + update_with_info_json + Whether to actually download or update using existing info json dry_run Whether to dry run or not @@ -101,7 +104,10 @@ def _download_subscriptions_from_yaml_files( logger.debug("Subscription full yaml:\n%s", subscription.as_yaml()) try: - transaction_log = subscription.download(dry_run=dry_run) + if update_with_info_json: + transaction_log = subscription.update_with_info_json(dry_run=dry_run) + else: + transaction_log = subscription.download(dry_run=dry_run) except Exception as exc: # pylint: disable=broad-except _maybe_write_subscription_log_file( config=config, subscription=subscription, dry_run=dry_run, exception=exc @@ -305,9 +311,21 @@ def main() -> List[Tuple[Subscription, FileHandlerTransactionLog]]: with working_directory_lock(config=config): if args.subparser == "sub": + if ( + args.update_with_info_json + and not config.config_options.experimental.enable_update_with_info_json + ): + raise ExperimentalFeatureNotEnabled( + "--update-with-info-json requires setting " + "configuration.experimental.update_with_info_json to True. This feature is ", + "still being tested and has the ability to destroy files. Ensure you have a ", + "full backup before usage. You have been warned!", + ) + transaction_logs = _download_subscriptions_from_yaml_files( config=config, subscription_paths=args.subscription_paths, + update_with_info_json=args.update_with_info_json, dry_run=args.dry_run, ) diff --git a/src/ytdl_sub/cli/main_args_parser.py b/src/ytdl_sub/cli/main_args_parser.py index e5879f9e..35f63849 100644 --- a/src/ytdl_sub/cli/main_args_parser.py +++ b/src/ytdl_sub/cli/main_args_parser.py @@ -135,6 +135,13 @@ _add_shared_arguments(parser, suppress_defaults=False) subparsers = parser.add_subparsers(dest="subparser") ################################################################################################### # SUBSCRIPTION PARSER +class SubArguments: + UPDATE_WITH_INFO_JSON = CLIArgument( + short="-u", + long="--update-with-info-json", + ) + + subscription_parser = subparsers.add_parser("sub") _add_shared_arguments(subscription_parser, suppress_defaults=True) subscription_parser.add_argument( @@ -144,6 +151,14 @@ subscription_parser.add_argument( help="path to subscription files, uses subscriptions.yaml if not provided", default=["subscriptions.yaml"], ) +subscription_parser.add_argument( + SubArguments.UPDATE_WITH_INFO_JSON.short, + SubArguments.UPDATE_WITH_INFO_JSON.long, + action="store_true", + help="update all subscriptions with the current config using info.json files", + default=False, +) + ################################################################################################### # DOWNLOAD PARSER download_parser = subparsers.add_parser("dl") diff --git a/src/ytdl_sub/config/config_validator.py b/src/ytdl_sub/config/config_validator.py index 04510b30..4fb643c9 100644 --- a/src/ytdl_sub/config/config_validator.py +++ b/src/ytdl_sub/config/config_validator.py @@ -24,6 +24,27 @@ else: _DEFAULT_FFPROBE_PATH = "/usr/bin/ffprobe" +class ExperimentalValidator(StrictDictValidator): + _optional_keys = {"enable_update_with_info_json"} + _allow_extra_keys = True + + def __init__(self, name: str, value: Any): + super().__init__(name, value) + + self._enable_update_with_info_json = self._validate_key( + key="enable_update_with_info_json", validator=BoolValidator, default=False + ) + + @property + def enable_update_with_info_json(self) -> bool: + """ + Enables modifying subscription files using info.json files using the argument + ``--update-with-info-json``. This feature is still being tested and has the ability to + destroy files. Ensure you have a full backup before usage. You have been warned! + """ + return self._enable_update_with_info_json.value + + class PersistLogsValidator(StrictDictValidator): _required_keys = {"logs_directory"} _optional_keys = {"keep_logs_after", "keep_successful_logs"} @@ -79,6 +100,7 @@ class PersistLogsValidator(StrictDictValidator): return self._keep_successful_logs.value +# pylint: disable=too-many-instance-attributes class ConfigOptions(StrictDictValidator): _required_keys = {"working_directory"} _optional_keys = { @@ -88,6 +110,7 @@ class ConfigOptions(StrictDictValidator): "lock_directory", "ffmpeg_path", "ffprobe_path", + "experimental", } def __init__(self, name: str, value: Any): @@ -114,6 +137,9 @@ class ConfigOptions(StrictDictValidator): self._ffprobe_path = self._validate_key( key="ffprobe_path", validator=FFprobeFileValidator, default=_DEFAULT_FFPROBE_PATH ) + self._experimental = self._validate_key( + key="experimental", validator=ExperimentalValidator, default={} + ) @property def working_directory(self) -> str: @@ -167,6 +193,13 @@ class ConfigOptions(StrictDictValidator): """ return self._persist_logs + @property + def experimental(self) -> ExperimentalValidator: + """ + Experimental validator. readthedocs in the validator itself! + """ + return self._experimental + @property def lock_directory(self) -> str: """ diff --git a/src/ytdl_sub/downloaders/info_json/info_json_downloader.py b/src/ytdl_sub/downloaders/info_json/info_json_downloader.py index be1a4e9c..4c725e6b 100644 --- a/src/ytdl_sub/downloaders/info_json/info_json_downloader.py +++ b/src/ytdl_sub/downloaders/info_json/info_json_downloader.py @@ -88,7 +88,7 @@ class InfoJsonDownloader(BaseDownloader[InfoJsonDownloaderOptions]): file_names_mtime: Dict[str, Dict[str, float]] = defaultdict(dict) entries: List[Entry] = [] - for download_mapping in self._enhanced_download_archive.mapping._entry_mappings.values(): + for download_mapping in self._enhanced_download_archive.mapping.entry_mappings.values(): entry = self._get_entry_from_download_mapping(download_mapping) entries.append(entry) @@ -117,7 +117,7 @@ class InfoJsonDownloader(BaseDownloader[InfoJsonDownloaderOptions]): the working directory """ # Use original mapping since the live mapping gets wiped - entry_file_names = self._original_mapping._entry_mappings[entry.uid].file_names + entry_file_names = self._original_mapping.entry_mappings[entry.uid].file_names for file_name in entry_file_names: ext = get_file_extension(file_name) diff --git a/src/ytdl_sub/utils/exceptions.py b/src/ytdl_sub/utils/exceptions.py index 1a545b69..bfc222ec 100644 --- a/src/ytdl_sub/utils/exceptions.py +++ b/src/ytdl_sub/utils/exceptions.py @@ -36,3 +36,7 @@ class InvalidDlArguments(ValidationException): class FileNotDownloadedException(ValueError): """ytdlp failed to download something""" + + +class ExperimentalFeatureNotEnabled(ValidationException): + """Feature is not enabled for usage""" diff --git a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py index c04ce8f1..79193a86 100644 --- a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +++ b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py @@ -175,6 +175,15 @@ class DownloadMappings: download_mappings._entry_mappings = entry_mappings_json return download_mappings + @property + def entry_mappings(self) -> Dict[str, DownloadMapping]: + """ + Returns + ------- + Mapping of entries to files + """ + return self._entry_mappings + @property def entry_ids(self) -> List[str]: """ diff --git a/tests/unit/cli/test_main.py b/tests/unit/cli/test_main.py index d7e93f9b..b783843d 100644 --- a/tests/unit/cli/test_main.py +++ b/tests/unit/cli/test_main.py @@ -23,6 +23,7 @@ from ytdl_sub.cli.main import logger as main_logger from ytdl_sub.cli.main import main from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.file_handler import FileMetadata @@ -127,7 +128,10 @@ def test_subscription_logs_write_to_file( ): try: _download_subscriptions_from_yaml_files( - config=config, subscription_paths=subscription_paths, dry_run=dry_run + config=config, + subscription_paths=subscription_paths, + update_with_info_json=False, + dry_run=dry_run, ) except ValueError: assert not mock_success_output @@ -275,3 +279,22 @@ def test_output_summary(): _ = _output_summary(transaction_logs=mock_subscriptions) assert True # Test used for manual inspection - too hard to test ansi color codes + + +def test_update_with_info_json_requires_experimental_flag( + music_video_config_path: Path, + music_video_subscription_path: Path, +) -> None: + with patch.object( + sys, + "argv", + [ + "ytdl-sub", + "--config", + str(music_video_config_path), + "sub", + str(music_video_subscription_path), + "--update-with-info-json", + ], + ), pytest.raises(ExperimentalFeatureNotEnabled): + _ = main()