import os.path import re from pathlib import Path import pytest from ytdl_sub.config.preset import Preset from ytdl_sub.plugins.nfo_tags import NfoTagsOptions from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException from ytdl_sub.utils.exceptions import ValidationException class TestPreset: @pytest.mark.parametrize( "download_value", [ {"url": "youtube.com/watch?v=123abc"}, # url download strategy {"urls": [{"url": "youtube.com/watch?v=123abc"}]}, # multi-url download strategy "youtube.com/watch?v=123abc", # single string ["youtube.com/watch?v=123abc", "youtube.com/watch?v=123xyz"], # list of strings [{"url": "youtube.com/watch?v=123abc"}, "youtube.com/watch?v=123abc"], # dict and str ], ) def test_bare_minimum_preset(self, config_file, output_options, download_value): _ = Preset( config=config_file, name="test", value={"download": download_value, "output_options": output_options}, ) def test_preset_with_override_variable(self, config_file, output_options, youtube_video): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, "overrides": {"dne_var": "not dne"}, }, ) def test_preset_with_output_directory_tilda(self, config_file, output_options, youtube_video): out = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "~/output/dir", "file_name": "{dne_var}"}, "overrides": {"dne_var": "not dne"}, }, ) assert out.overrides.apply_formatter(out.output_options.output_directory) == str( Path(os.path.expanduser("~")) / "output" / "dir" ) def test_preset_parent(self, config_file, output_options, youtube_video): preset = Preset( config=config_file, name="test", value={ "preset": "parent_preset_1", "download": youtube_video, "output_options": output_options, "nfo_tags": {"tags": {"key-2": "this-preset"}}, }, ) nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions) tags_string_dict = { key: formatter[0].format_string for key, formatter in nfo_options.tags.string_tags.items() } assert tags_string_dict == {"key-1": "preset_0", "key-2": "this-preset"} @pytest.mark.parametrize( "preset_value", [["parent_preset_1", "parent_preset_2"], "parent_preset_3"] ) def test_preset_multiple_parents( self, config_file, output_options, youtube_video, preset_value ): preset = Preset( config=config_file, name="test", value={ "preset": preset_value, "download": youtube_video, "output_options": output_options, "nfo_tags": {"tags": {"key-3": "this-preset"}}, }, ) nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions) tags_string_dict = { key: formatter[0].format_string for key, formatter in nfo_options.tags.string_tags.items() } assert tags_string_dict == { "key-1": "preset_0", "key-2": "preset_2", "key-3": "this-preset", } def test_preset_datetime_with_override(self, config_file, youtube_video, output_options): preset = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": dict( output_options, **{"maintain_download_archive": True, "keep_files_after": "today-{ttl}"}, ), "overrides": {"ttl": "2months"}, }, ) assert ( preset.overrides.apply_formatter(formatter=preset.output_options.keep_files_after) == "today-2months" ) @pytest.mark.parametrize( "parent_preset", ["preset_self_loop", "preset_loop_0", "preset_loop_1"] ) def test_preset_error__parent_loop( self, config_file, output_options, youtube_video, parent_preset ): with pytest.raises(ValidationException, match="preset loop detected"): _ = Preset( config=config_file, name="test", value={ "preset": parent_preset, "download": youtube_video, "output_options": output_options, }, ) def test_preset_error__source_variable_does_not_exist( self, config_file, output_options, youtube_video ): with pytest.raises( StringFormattingVariableNotFoundException, match="contains the following variables that do not exist: dne_var", ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, }, ) def test_preset_error__override_variable_does_not_exist( self, config_file, output_options, youtube_video ): with pytest.raises( StringFormattingVariableNotFoundException, match="contains the following variables that do not exist: dne_var", ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "{dne_var}", "file_name": "file"}, }, ) def test_preset_error__dict_source_variable_does_not_exist( self, config_file, output_options, youtube_video ): with pytest.raises( StringFormattingVariableNotFoundException, match="contains the following variables that do not exist: dne_var", ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "dir", "file_name": "file"}, "nfo_tags": { "nfo_name": "the nfo name", "nfo_root": "the root", "tags": {"tag_a": "{dne_var}"}, }, }, ) def test_preset_error__dict_override_variable_does_not_exist( self, config_file, output_options, youtube_video ): with pytest.raises( StringFormattingVariableNotFoundException, match="contains the following variables that do not exist: dne_var", ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": output_options, "output_directory_nfo_tags": { "nfo_name": "the nfo name", "nfo_root": "the root", "tags": {"tag_a": "{dne_var}"}, }, }, ) def test_preset_error__dict_override_variable_not_static( self, config_file, output_options, youtube_video ): with pytest.raises( StringFormattingVariableNotFoundException, match="static formatters must contain variables that " "have no dependency to entry variables", ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": { "output_directory": "{title}", "file_name": "{uid}", }, }, ) def test_preset_with_multi_url__contains_empty_url(self, config_file, output_options): _ = Preset( config=config_file, name="test", value={ "download": [{"url": "non-empty url"}, {"url": ""}], # empty url "output_options": output_options, }, ) @pytest.mark.parametrize( "entry_variable_name", [ "title", "playlist_uid", "source_title", "playlist_max_upload_year", ], ) def test_preset_error_override_variable_collides_with_entry_variable( self, config_file, output_options, youtube_video, entry_variable_name: str ): with pytest.raises( ValidationException, match=re.escape( f"Override variable with name {entry_variable_name} cannot be used since" " it is a built-in ytdl-sub entry variable name." ), ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, "overrides": {entry_variable_name: "fail"}, }, ) def test_preset_error_override_variable_collides_added_variable( self, config_file, output_options, youtube_video ): with pytest.raises( ValidationException, match=re.escape( f"Override variable with name subtitles_ext cannot be used since" " it is added by a plugin." ), ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "dir", "file_name": "ack"}, "subtitles": { "embed_subtitles": True, }, "overrides": {"subtitles_ext": "collide"}, }, ) @pytest.mark.parametrize( "function_name", [ "%extract_field_from_siblings", "%sanitize", "%array", ], ) def test_preset_error_override_variable_collides_with_custom_function( self, config_file, output_options, youtube_video, function_name: str ): with pytest.raises( ValidationException, match=re.escape( f"Override function definition with name {function_name} cannot be used since" " it is a built-in ytdl-sub function name." ), ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, "overrides": {function_name: "fail"}, }, ) def test_preset_error_override_added_variable_collides_with_built_in( self, config_file, output_options ): with pytest.raises( ValidationException, match=re.escape( "Cannot use the variable name title because it exists as a " "built-in ytdl-sub variable name." ), ): _ = Preset( config=config_file, name="test", value={ "download": { "url": "youtube.com/watch?v=123abc", "variables": {"title": "nope"}, }, "output_options": {"output_directory": "dir", "file_name": "acjk"}, }, ) def test_preset_error_override_added_variable_collides_with_override( self, config_file, output_options ): with pytest.raises( ValidationException, match=re.escape( "Override variable with name the_bad_one cannot be used since " "it is added by a plugin." ), ): _ = Preset( config=config_file, name="test", value={ "download": { "url": "youtube.com/watch?v=123abc", "variables": {"the_bad_one": "should error"}, }, "output_options": {"output_directory": "dir", "file_name": "acjk"}, "overrides": {"the_bad_one": "ack"}, }, ) @pytest.mark.parametrize( "name", ["!ack", "*asfsaf", "1234352", "--234asdf", "___asdf", "1asdfasdfasd"] ) @pytest.mark.parametrize("is_function", [True, False]) def test_preset_error_overrides_invalid_variable_name( self, config_file, youtube_video, output_options, name: str, is_function: bool ): name_type = "function" if is_function else "variable" name = f"%{name}" if is_function else name with pytest.raises( ValidationException, match=re.escape( f"Override {name_type} with name {name} is invalid." " Names must be lower_snake_cased and begin with a letter." ), ): _ = Preset( config=config_file, name="test", value={ "download": youtube_video, "output_options": output_options, "overrides": {name: "ack"}, }, ) def test_preset_error_added_url_variable_cannot_resolve(self, config_file, output_options): with pytest.raises( ValidationException, match=re.escape( "variable the_bad_one cannot use the variables subtitles_ext because it " "depends on other variables that are computed later in execution" ), ): _ = Preset( config=config_file, name="test", value={ "download": { "url": "youtube.com/watch?v=123abc", "variables": {"the_bad_one": "{subtitles_ext}"}, }, "subtitles": { "embed_subtitles": True, }, "output_options": {"output_directory": "dir", "file_name": "acjk"}, }, )