diff --git a/src/ytdl_sub/cli/entrypoint.py b/src/ytdl_sub/cli/entrypoint.py index 2247c4d4..5fdf868d 100644 --- a/src/ytdl_sub/cli/entrypoint.py +++ b/src/ytdl_sub/cli/entrypoint.py @@ -15,8 +15,9 @@ from ytdl_sub.cli.output_transaction_log import ( ) from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub from ytdl_sub.cli.parsers.dl import DownloadArgsParser -from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, InspectArguments, parser +from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, parser from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.config.validators.variable_validation import ResolutionLevel from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled, ValidationException from ytdl_sub.utils.file_handler import FileHandler @@ -215,14 +216,6 @@ def _parse_inspect_mocks(mocks: Optional[List[str]]) -> Dict[str, str]: return out -def _parse_inspect_level(inspect_level: str) -> int: - for val, name in InspectArguments.LevelChoices.items(): - if inspect_level == val or inspect_level == name: - return int(val) - - raise ValueError("should not reach here") - - def _inspect( config: ConfigFile, subscription_paths: List[str], @@ -288,7 +281,7 @@ def main() -> List[Subscription]: subscription_paths=args.subscription_paths, subscription_matches=args.match, subscription_override_dict=subscription_override_dict, - inspection_level=_parse_inspect_level(args.inspection_level), + inspection_level=ResolutionLevel.level_number(args.inspection_level), mocks=_parse_inspect_mocks(args.mock), ) return [] diff --git a/src/ytdl_sub/cli/parsers/main.py b/src/ytdl_sub/cli/parsers/main.py index 6e98efd4..5fecb463 100644 --- a/src/ytdl_sub/cli/parsers/main.py +++ b/src/ytdl_sub/cli/parsers/main.py @@ -266,12 +266,12 @@ inspect_parser.add_argument( metavar=",".join(str(i) for i in range(4)), type=str, help="""level of inspection to perform: - 0 - original present the subscription as-is (default) + 0 - original present the subscription as-is 1 - fill fill in defined values - 2 - resolve resolve all variables + 2 - resolve resolve all possible variables (default) 3 - internal resolve all variables to their internal representation """, - default="0", + default="resolve", choices=list(InspectArguments.LevelChoices.keys()) + list(InspectArguments.LevelChoices.values()), dest="inspection_level", diff --git a/src/ytdl_sub/config/validators/variable_validation.py b/src/ytdl_sub/config/validators/variable_validation.py index 42645d84..f90d803c 100644 --- a/src/ytdl_sub/config/validators/variable_validation.py +++ b/src/ytdl_sub/config/validators/variable_validation.py @@ -35,6 +35,21 @@ class ResolutionLevel: return "internal" raise ValueError("Invalid resolution level") + @classmethod + def level_number(cls, resolution_arg: str) -> int: + """ + Numeric resolution level + """ + if resolution_arg in ("0", "original"): + return 0 + if resolution_arg in ("1", "fill"): + return 1 + if resolution_arg in ("2", "resolve"): + return 2 + if resolution_arg in ("3", "internal"): + return 3 + raise ValueError("Invalid resolution level") + @classmethod def all(cls) -> List[int]: """ @@ -81,8 +96,6 @@ class VariableValidation: unresolvable=self.unresolved_variables, ) - - self.script = self.script.resolve_partial( unresolvable=self.unresolved_variables, output_filter=self._get_resolve_partial_filter(), @@ -132,9 +145,7 @@ class VariableValidation: value = self.script.definition_of(name) if name in self.script.function_names: # Keep custom functions as-is - output[name] = self.overrides.dict_with_format_strings[ - name - ] + output[name] = self.overrides.dict_with_format_strings[name] elif resolved := value.maybe_resolvable: output[name] = resolved.native else: diff --git a/tests/integration/cli/test_inspect.py b/tests/integration/cli/test_inspect.py index b11920db..e7136317 100644 --- a/tests/integration/cli/test_inspect.py +++ b/tests/integration/cli/test_inspect.py @@ -3,10 +3,16 @@ from pathlib import Path import pytest import yaml from conftest import mock_run_from_cli +from unit.config.test_subscription_resolution import compare_resolved_yaml + +from ytdl_sub.config.validators.variable_validation import ResolutionLevel class TestInspect: @pytest.mark.parametrize("config_provided", [True, False]) + @pytest.mark.parametrize( + "inspect_level", ["0", "original", "1", "fill", "2", "resolve", "3", "internal"] + ) def test_inspect_command( self, capsys, @@ -14,238 +20,21 @@ class TestInspect: tv_show_subscriptions_path: Path, output_directory: str, config_provided: bool, + inspect_level: str, ): + # Shares same test fixture as `test_subscription_resolution.py` args = f"--config {default_config_path} " if config_provided else "" - args += f"inspect {tv_show_subscriptions_path} --match 'Jake Trains'" + args += f"inspect {tv_show_subscriptions_path} --match 'NOVA PBS' " + args += f"--level {inspect_level}" subscriptions = mock_run_from_cli(args=args) assert len(subscriptions) == 0 - captured_as_dict = yaml.safe_load(capsys.readouterr().out) - assert captured_as_dict == { - "chapters": { - "allow_chapters_from_comments": False, - "embed_chapters": True, - "enable": "True", - "force_key_frames": False, - }, - "date_range": {"breaks": "True", "enable": "True", "type": "upload_date"}, - "download": [ - { - "download_reverse": "True", - "include_sibling_metadata": False, - "playlist_thumbnails": [ - { - "name": "{avatar_uncropped_thumbnail_file_name}", - "uid": "avatar_uncropped", - }, - { - "name": "{banner_uncropped_thumbnail_file_name}", - "uid": "banner_uncropped", - }, - ], - "source_thumbnails": [ - { - "name": "{avatar_uncropped_thumbnail_file_name}", - "uid": "avatar_uncropped", - }, - { - "name": "{banner_uncropped_thumbnail_file_name}", - "uid": "banner_uncropped", - }, - ], - "url": "https://www.youtube.com/@JakeTrains", - "variables": {}, - "webpage_url": "{modified_webpage_url}", - "ytdl_options": {}, - } - ], - "file_convert": {"convert_to": "mp4", "convert_with": "yt-dlp", "enable": "True"}, - "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", - "output_options": { - "download_archive_name": ".ytdl-sub-Jake Trains-download-archive.json", - "file_name": "{episode_file_path}.{ext}", - "info_json_name": "{episode_file_path}.{info_json_ext}", - "keep_files_date_eval": "{episode_date_standardized}", - "maintain_download_archive": True, - "output_directory": f"{output_directory}/Jake Trains", - "preserve_mtime": False, - "thumbnail_name": "{thumbnail_file_name}", - }, - "overrides": { - "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", - "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", - "%is_bilateral_url": '{ %contains( $0, "youtube.com/playlist" ) }', - "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", - "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", - "assert_not_collection": '{ %assert( %and( %not( %bool( s01_url ) ), %not( %bool( s01_name ) ) ), "Provided `s01_url` or `s01_name` variable to TV Show by Date preset when it expects `url`. Perhaps you meant to use the `TV Show Collection` preset?" ) }', - "avatar_uncropped_thumbnail_file_name": "{ tv_show_poster_file_name }", - "banner_uncropped_thumbnail_file_name": "{ tv_show_fanart_file_name }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "episode_content_rating": "{ tv_show_content_rating }", - "episode_date_standardized": '{ %if( %contains( tv_show_by_date_season_ordering, "release" ), release_date_standardized, upload_date_standardized ) }', - "episode_file_name": '{ %concat( %string( "s" ), %string( season_number_padded ), %string( ".e" ), %string( episode_number_padded ), %string( " - " ), %string( file_title ) ) }', - "episode_file_path": '{ %concat( %string( season_directory_name_sanitized ), %string( "/" ), %string( episode_file_name_sanitized ) ) }', - "episode_number": "{ %array_at( episode_number_and_padded_, 0 ) }", - "episode_number_and_padded_": '{ %elif( %episode_ordering_( "upload-day" ), [ %concat( upload_day, upload_date_index_padded ), 4 ], %episode_ordering_( "upload-month-day" ), [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ], %episode_ordering_( "upload-month-day-reversed" ), [ %concat( upload_day_of_year_reversed, upload_date_index_reversed_padded ), 5 ], %episode_ordering_( "release-day" ), [ %concat( release_day, upload_date_index_padded ), 4 ], %episode_ordering_( "release-month-day" ), [ %concat( release_month, release_day_padded, upload_date_index_padded ), 6 ], %episode_ordering_( "release-month-day-reversed" ), [ %concat( release_day_of_year_reversed, upload_date_index_reversed_padded ), 5 ], %episode_ordering_( "download-index" ), [ download_index, 6 ], %throw( \'tv_show_by_date_episode_ordering must be one of the following: "upload-day", "upload-month-day", "upload-month-day-reversed", "release-day", "release-month-day", "release-month-day-reversed", "download-index"\' ) ) }', - "episode_number_padded": "{ %pad_zero( %int( episode_number ), %int( %array_at( episode_number_and_padded_, 1 ) ) ) }", - "episode_plot": '{ %concat( %string( webpage_url ), %string( "\n\n" ), %string( description ) ) }', - "episode_title": '{ %concat( %string( episode_date_standardized ), %string( " - " ), %string( title ) ) }', - "episode_year": "{ %slice( episode_date_standardized, 0, 4 ) }", - "file_title": "{ title_sanitized_plex }", - "file_uid": "{ uid_sanitized_plex }", - "include_sibling_metadata": "{ %bool(False) }", - "modified_webpage_url": "{ webpage_url }", - "only_recent_date_range": "2months", - "only_recent_max_files": "{ %int(30) }", - "resolution_assert": '{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( "Entry ", title, " downloaded at a low resolution (", resolution_readable, "), you\'ve probably been throttled. ", "Stopping further downloads, wait a few hours and try again. ", "Disable using the override variable `enable_resolution_assert: False`." ) ), "false is no-op" ) }', - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": '{ %print_if_true( %concat( title, " has a match in resolution_assert_ignore_titles, skipping resolution assert." ), %contains_any( title, resolution_assert_ignore_titles ) ) }', - "resolution_assert_print": '{ %print( %if( enable_resolution_assert, "Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`", "Resolution assert is disabled. Use at your own risk!" ), enable_resolution_assert, -1 ) }', - "resolution_readable": '{ %concat( %string( width ), %string( "x" ), %string( height ) ) }', - "s01_name": "", - "s01_url": "", - "season_directory_name": '{ %concat( %string( "Season " ), %string( season_number_padded ) ) }', - "season_number": '{ %elif( %season_ordering_( "upload-year" ), upload_year, %season_ordering_( "upload-year-month" ), %concat( upload_year, upload_month_padded ), %season_ordering_( "release-year" ), release_year, %season_ordering_( "release-year-month" ), %concat( release_year, release_month_padded ), %throw( \'tv_show_by_date_season_ordering must be one of the following: "upload-year", "upload-year-month", "release-year", "release-year-month"\' ) ) }', - "season_number_padded": "{ season_number }", - "season_poster_file_name": '{ %concat( %string( season_directory_name_sanitized ), %string( "/Season" ), %string( season_number_padded ), %string( ".jpg" ) ) }', - "subscription_array": "{ %from_json( '[\"https://www.youtube.com/@JakeTrains\"]' ) }", - "subscription_indent_1": "Kids", - "subscription_indent_2": "TV-Y", - "subscription_value": "https://www.youtube.com/@JakeTrains", - "subscription_value_1": "https://www.youtube.com/@JakeTrains", - "thumbnail_file_name": '{ %concat( %string( episode_file_path ), %string( "-thumb.jpg" ) ) }', - "tv_show_by_date_episode_ordering": "upload-month-day", - "tv_show_by_date_ordering_pair_validation_": '{ %assert_then( %or( %ordering_pair_eq( "upload-year", "upload-month-day" ), %ordering_pair_eq( "upload-year", "upload-month-day-reversed" ), %ordering_pair_eq( "upload-year", "download-index" ), %ordering_pair_eq( "upload-year-month", "upload-day" ), %ordering_pair_eq( "release-year", "release-month-day" ), %ordering_pair_eq( "release-year", "release-month-day-reversed" ), %ordering_pair_eq( "release-year", "download-index" ), %ordering_pair_eq( "release-year-month", "release-day" ) ), episode_number_and_padded_, "Detected incompatibility between tv_show_by_date_season_ordering and tv_show_by_date_episode_ordering. Ensure you are not using both upload and release date, and that the year/month/day are included in the combined season and episode." ) }', - "tv_show_by_date_season_ordering": "upload-year", - "tv_show_content_rating": "{ subscription_indent_2 }", - "tv_show_content_rating_default": "TV-14", - "tv_show_date_range_type": '{ %if( %contains( tv_show_by_date_season_ordering, "release" ), "release_date", "upload_date" ) }', - "tv_show_directory": output_directory, - "tv_show_fanart_file_name": "fanart.jpg", - "tv_show_genre": "{ subscription_indent_1 }", - "tv_show_genre_default": "ytdl-sub", - "tv_show_name": "{ subscription_name }", - "tv_show_poster_file_name": "poster.jpg", - "url": "{ subscription_value }", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ subscription_array }", - }, - "throttle_protection": { - "enable": True, - "sleep_per_download_s": {"max": "28.4", "min": "13.8"}, - "sleep_per_request_s": {"max": "0.75", "min": "0.0"}, - "sleep_per_subscription_s": {"max": "26.1", "min": "16.3"}, - }, - "video_tags": { - "contentRating": "{episode_content_rating}", - "date": "{episode_date_standardized}", - "episode_id": "{episode_number}", - "genre": "{tv_show_genre}", - "show": "{tv_show_name}", - "synopsis": "{episode_plot}", - "title": "{episode_title}", - "year": "{episode_year}", - }, - } + out = yaml.safe_load(capsys.readouterr().out) + compare_resolved_yaml( + out=out, + output_directory=output_directory, + subscription_name="NOVA PBS", + preset_type="tv_show", + resolution_level=ResolutionLevel.level_number(resolution_arg=inspect_level), + ) diff --git a/tests/unit/config/test_subscription_resolution.py b/tests/unit/config/test_subscription_resolution.py index 12781bb8..dd6b77e8 100644 --- a/tests/unit/config/test_subscription_resolution.py +++ b/tests/unit/config/test_subscription_resolution.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Dict import pytest import yaml @@ -10,12 +11,13 @@ from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.file_path import FilePathTruncater -def _ensure_resolved_yaml( - sub: Subscription, output_directory: str, preset_type: str, resolution_level: int +def compare_resolved_yaml( + out: Dict, + output_directory: str, + subscription_name: str, + preset_type: str, + resolution_level: int, ) -> None: - output_yaml = sub.resolved_yaml(resolution_level=resolution_level) - out = yaml.safe_load(output_yaml) - expected_out_filename = ( f"{preset_type}/inspect_sub_{ResolutionLevel.name_of(resolution_level)}.json" ) @@ -24,7 +26,7 @@ def _ensure_resolved_yaml( if resolution_level > ResolutionLevel.ORIGINAL: output_path = Path(output_directory) if "tv_show_directory" in expected_out["overrides"]: - output_path = output_path / sub.name + output_path = output_path / subscription_name expected_out["output_options"]["output_directory"] = FilePathTruncater.to_native_filepath( str(output_path) @@ -40,6 +42,21 @@ def _ensure_resolved_yaml( assert out == expected_out +def _ensure_resolved_yaml( + sub: Subscription, output_directory: str, preset_type: str, resolution_level: int +) -> None: + output_yaml = sub.resolved_yaml(resolution_level=resolution_level) + out = yaml.safe_load(output_yaml) + + compare_resolved_yaml( + out=out, + output_directory=output_directory, + subscription_name=sub.name, + preset_type=preset_type, + resolution_level=resolution_level, + ) + + @pytest.mark.parametrize("resolution_level", ResolutionLevel.all()) class TestResolution: def test_resolution_tv_show(