diff --git a/main.py b/main.py index 6725540..6c4f7a8 100755 --- a/main.py +++ b/main.py @@ -68,7 +68,7 @@ def shutdown(): if __name__ == "__main__": - config = settings.check_toml("utils/.config.template.toml", "config.toml") + config = settings.check_config(template_name="utils/.config.template.toml", name="config.toml") config is False and exit() try: if config["settings"]["times_to_run"]: diff --git a/utils/.config.template.toml b/utils/.config.template.toml index d783349..45a481b 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -1,46 +1,203 @@ +[reddit] + [reddit.creds] -client_id = { optional = false, nmin = 12, nmax = 30, explanation = "the ID of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The ID should be over 12 and under 30 characters, double check your input." } -client_secret = { optional = false, nmin = 20, nmax = 40, explanation = "the SECRET of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The secret should be over 20 and under 40 characters, double check your input." } -username = { optional = false, nmin = 3, nmax = 20, explanation = "the username of your reddit account", example = "JasonLovesDoggo", regex = "^[-_0-9a-zA-Z]+$", oob_error = "A username HAS to be between 3 and 20 characters" } -password = { optional = false, nmin = 8, explanation = "the password of your reddit account", example = "fFAGRNJru1FTz70BzhT3Zg", oob_error = "Password too short" } -2fa = { optional = true, type = "bool", options = [true, - false, -], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true } +[reddit.creds.client_id] +optional = false +nmin = 12 +nmax = 30 +explanation = "the ID of your Reddit app of SCRIPT type" +example = "fFAGRNJru1FTz70BzhT3Zg" +regex = "^[-a-zA-Z0-9._~+/]+=*$" +input_error = "The client ID can only contain printable characters." +oob_error = "The ID should be over 12 and under 30 characters, double check your input." + +[reddit.creds.client_secret] +optional = false +nmin = 20 +nmax = 40 +explanation = "the SECRET of your Reddit app of SCRIPT var_type" +example = "fFAGRNJru1FTz70BzhT3Zg" +regex = "^[-a-zA-Z0-9._~+/]+=*$" +input_error = "The client ID can only contain printable characters." +oob_error = "The secret should be over 20 and under 40 characters, double check your input." + +[reddit.creds.username] +optional = false +nmin = 3 +nmax = 20 +explanation = "the username of your reddit account" +example = "JasonLovesDoggo" +regex = "^[-_0-9a-zA-Z]+$" +oob_error = "A username HAS to be between 3 and 20 characters" + +[reddit.creds.password] +optional = false +nmin = 8 +explanation = "the password of your reddit account" +example = "fFAGRNJru1FTz70BzhT3Zg" +oob_error = "Password too short" + +[reddit.creds.2fa] +optional = true +var_type = "bool" +options = [true, false] +default = false +explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False" +example = true [reddit.thread] -random = { optional = true, options = [true, - false, -], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" } -subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "what subreddit to pull posts from, the name of the sub, not the URL", example = "AskReddit", oob_error = "A subreddit name HAS to be between 3 and 20 characters" } -post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z0-9])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" } -max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, type = "int", explanation = "max number of characters a comment can have. default is 500", example = 500, oob_error = "the max comment length should be between 10 and 10000" } -post_lang = { default = "", optional = true, explanation = "The language you would like to translate to.", example = "es-cr" } -min_comments = { default = 20, optional = false, nmin = 15, type = "int", explanation = "The minimum number of comments a post should have to be included. default is 20", example = 29, oob_error = "the minimum number of comments should be between 15 and 999999" } +[reddit.thread.random] +optional = true +options = [true, false] +default = false +var_type = "bool" +explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'" +example = "True" + +[reddit.thread.subreddit] +optional = false +regex = "[_0-9a-zA-Z]+$" +nmin = 3 +explanation = "what subreddit to pull posts from, the name of the sub, not the URL" +example = "AskReddit" +oob_error = "A subreddit name HAS to be between 3 and 20 characters" + +[reddit.thread.post_id] +optional = true +default = "" +regex = "^((?!://|://)[+a-zA-Z0-9])*$" +explanation = "Used if you want to use a specific post." +example = "urdtfx" + +[reddit.thread.max_comment_length] +default = 500 +optional = false +nmin = 10 +nmax = 10000 +var_type = "int" +explanation = "max number of characters a comment can have. default is 500" +example = 500 +oob_error = "the max comment length should be between 10 and 10000" + +[reddit.thread.post_lang] +default = "" +optional = true +explanation = "The language you would like to translate to." +example = "es-cr" + +[reddit.thread.min_comments] +default = 20 +optional = false +nmin = 15 +var_type = "int" +explanation = "The minimum number of comments a post should have to be included. default is 20" +example = 29 +oob_error = "the minimum number of comments should be between 15 and 999999" + [settings] -allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, - false, -], explanation = "Whether to allow NSFW content, True or False" } -theme = { optional = false, default = "dark", example = "light", options = ["dark", - "light", -], explanation = "sets the Reddit theme, either LIGHT or DARK" } -times_to_run = { optional = false, default = 1, example = 2, explanation = "used if you want to run multiple times. set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." } -opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" } -transition = { optional = true, default = 0.2, example = 0.2, explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it.", type = "float", nmin = 0, nmax = 2, oob_error = "The transition HAS to be between 0 and 2", input_error = "The opacity HAS to be a decimal number between 0 and 2" } -storymode = { optional = true, type = "bool", default = false, example = false, options = [true, - false, -], explanation = "not yet implemented" } +[settings.allow_nsfw] +optional = false +var_type = "bool" +default = false +example = false +options = [true, false] +explanation = "Whether to allow NSFW content, True or False" + +[settings.theme] +optional = false +default = "dark" +example = "light" +options = ["dark", "light"] +explanation = "sets the Reddit theme, either LIGHT or DARK" + +[settings.times_to_run] +optional = false +default = 1 +example = 2 +explanation = "used if you want to run multiple times. set to an int e.g. 4 or 29 or 1" +var_type = "int" +nmin = 1 +oob_error = "It's very hard to run something less than once." + +[settings.opacity] +optional = false +default = 0.9 +example = 0.8 +explanation = "Sets the opacity of the comments when overlayed over the background" +var_type = "float" +nmin = 0 +nmax = 1 +oob_error = "The opacity HAS to be between 0 and 1" +input_error = "The opacity HAS to be a decimal number between 0 and 1" + +[settings.transition] +optional = true +default = 0.2 +example = 0.2 +explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it." +var_type = "float" +nmin = 0 +nmax = 2 +oob_error = "The transition HAS to be between 0 and 2" +input_error = "The opacity HAS to be a decimal number between 0 and 2" + +[settings.storymode] +optional = true +var_type = "bool" +default = false +example = false +options = [true, false] +explanation = "not yet implemented" + [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video" } -#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, -# false, -#], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } -#background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" } +[settings.background.background_choice] +optional = true +default = "minecraft" +example = "minecraft" +options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""] +explanation = "Sets the background for the video" + +#[settings.background.background_audio] +#optional = true +#var_type = "bool" +#default = false +#example = false +#options = [true, false] +#explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" +# +#[settings.background.background_audio_volume] +#optional = true +#var_type = "float" +#default = 0.3 +#example = 0.1 +#explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" + [settings.tts] -choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ], example = "streamlabspolly", explanation = "The backend used for TTS generation. This can be left blank and you will be prompted to choose at runtime." } -aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } -streamlabs_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" } -tiktok_voice = { optional = false, default = "en_us_006", example = "en_us_006", explanation = "The voice used for TikTok TTS" } +[settings.tts.choice] +optional = false +default = "" +options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", ""] +example = "streamlabspolly" +explanation = "The backend used for TTS generation. This can be left blank and you will be prompted to choose at runtime." + +[settings.tts.aws_polly_voice] +optional = false +default = "Matthew" +example = "Matthew" +explanation = "The voice used for AWS Polly" + +[settings.tts.streamlabs_polly_voice] +optional = false +default = "Matthew" +example = "Matthew" +explanation = "The voice used for Streamlabs Polly" + +[settings.tts.tiktok_voice] +optional = false +default = "en_us_006" +example = "en_us_006" +explanation = "The voice used for TikTok TTS" diff --git a/utils/console.py b/utils/console.py index 6f99a41..7f15a08 100644 --- a/utils/console.py +++ b/utils/console.py @@ -5,6 +5,7 @@ from rich.padding import Padding from rich.panel import Panel from rich.text import Text from rich.columns import Columns +from typing import Optional, Union import re console = Console() @@ -36,18 +37,32 @@ def print_substep(text, style=""): def handle_input( - message: str = "", - check_type=False, - match: str = "", - err_message: str = "", - nmin=None, - nmax=None, - oob_error="", - extra_info="", - options: list = None, - default=NotImplemented, - optional=False, + *, + var_type: Union[str, bool] = False, + regex: str = "", + input_error: str = "", + nmin: Optional[int] = None, # noqa + nmax: Optional[int] = None, # noqa + oob_error: str = "", + explanation: str = "", + options: list = None, + default: Optional[str] = NotImplemented, + optional: bool = False, + example: Optional[str] = None, + name: str = "", + message: Optional[Union[str, float]] = None, ): + if not message: + message = ( + (("[blue]Example: " + str(example) + "\n") if example else "") + + "[red]" + + ("Non-optional ", "Optional ")[optional] + + "[#C0CAF5 bold]" + + str(name) + + "[#F7768E bold]=" + ) + var_type: any = eval(var_type) if var_type else var_type + if optional: console.print(message + "\n[green]This is an optional value. Do you want to skip it? (y/n)") if input().casefold().startswith("y"): @@ -63,16 +78,16 @@ def handle_input( if input().casefold().startswith("y"): return default if options is None: - match = re.compile(match) - console.print("[green bold]" + extra_info, no_wrap=True) + regex = re.compile(regex) + console.print("[green bold]" + explanation, no_wrap=True) while True: console.print(message, end="") user_input = input("").strip() - if check_type is not False: + if var_type is not False: try: - user_input = check_type(user_input) + user_input = var_type(user_input) if (nmin is not None and user_input < nmin) or ( - nmax is not None and user_input > nmax + nmax is not None and user_input > nmax ): # FAILSTATE Input out of bounds console.print("[red]" + oob_error) @@ -80,34 +95,34 @@ def handle_input( break # Successful type conversion and number in bounds except ValueError: # Type conversion failed - console.print("[red]" + err_message) + console.print("[red]" + input_error) continue - elif match != "" and re.match(match, user_input) is None: - console.print("[red]" + err_message + "\nAre you absolutely sure it's correct?(y/n)") + elif regex != "" and re.match(regex, user_input) is None: + console.print("[red]" + input_error + "\nAre you absolutely sure it's correct?(y/n)") if input().casefold().startswith("y"): break continue else: # FAILSTATE Input STRING out of bounds if (nmin is not None and len(user_input) < nmin) or ( - nmax is not None and len(user_input) > nmax + nmax is not None and len(user_input) > nmax ): console.print("[red bold]" + oob_error) continue break # SUCCESS Input STRING in bounds return user_input - console.print(extra_info, no_wrap=True) + console.print(explanation, no_wrap=True) while True: console.print(message, end="") user_input = input("").strip() - if check_type is not False: + if var_type is not False: try: - isinstance(eval(user_input), check_type) - return check_type(user_input) - except: + isinstance(eval(user_input), var_type) + return var_type(user_input) + except Exception: # noqa (Exception is fine, it's not too broad) console.print( "[red bold]" - + err_message + + input_error + "\nValid options are: " + ", ".join(map(str, options)) + "." @@ -116,5 +131,5 @@ def handle_input( if user_input in options: return user_input console.print( - "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." + "[red bold]" + input_error + "\nValid options are: " + ", ".join(map(str, options)) + "." ) diff --git a/utils/settings.py b/utils/settings.py index a9d7726..5ddb8cf 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,170 +1,260 @@ #!/usr/bin/env python import toml -from rich.console import Console -import re +from os.path import exists +from re import match +from typing import Optional, Union, TypeVar, Callable -from typing import Tuple, Dict +from utils.console import handle_input, console -from utils.console import handle_input +function = TypeVar("function", bound=Callable[..., object]) +config: Optional[dict] = None +config_name: str = "config.toml" +config_template_name: str = "utils/.config.template.toml" -console = Console() -config = dict # autocomplete +def crawl( + obj: dict, + func: function = lambda x, y: print(x, y), + path: Optional[list] = None, +) -> None: + """ + Crawls on values of the dict and executes func if found dict w/ settings -def crawl(obj: dict, func=lambda x, y: print(x, y, end="\n"), path=None): - if path is None: # path Default argument value is mutable + Args: + obj: Dict to be crawled on + func: Function to be executed with settings + path: List with keys of nested dict + """ + if not path: path = [] - for key in obj.keys(): - if type(obj[key]) is dict: - crawl(obj[key], func, path + [key]) + + for key, value in obj.items(): + if type(value) is dict and any([type(v) is dict for v in value.values()]): + crawl(value, func, path + [key]) continue - func(path + [key], obj[key]) + func(path + [key], value) + +def check( + value: any, + checks: dict, + name: str, +) -> any: + """ + Checks values and asks user for input if value is incorrect -def check(value, checks, name): - def get_check_value(key, default_result): - return checks[key] if key in checks else default_result + Args: + value: Values to check + checks: List of checks as a dict + name: Name of the value to be checked - incorrect = False - if value == {}: - incorrect = True - if not incorrect and "type" in checks: + Returns: + Correct value + """ + correct = True if value else False + + if correct and "type" in checks: try: value = eval(checks["type"])(value) - except: - incorrect = True + except Exception: # noqa (Exception is fine, it's not too broad) + correct = False if ( - not incorrect and "options" in checks and value not in checks["options"] + correct and "options" in checks and value not in checks["options"] ): # FAILSTATE Value is not one of the options - incorrect = True + correct = False if ( - not incorrect + correct and "regex" in checks and ( - (isinstance(value, str) and re.match(checks["regex"], value) is None) + (isinstance(value, str) and match(checks["regex"], value) is None) or not isinstance(value, str) ) ): # FAILSTATE Value doesn't match regex, or has regex but is not a string. - incorrect = True + correct = False if ( - not incorrect + correct and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) ) ): - incorrect = True + correct = False if ( - not incorrect + correct and hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) ) ): - incorrect = True - - if incorrect: - value = handle_input( - message=( - (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") - + "[red]" - + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] - ) - + "[#C0CAF5 bold]" - + str(name) - + "[#F7768E bold]=", - extra_info=get_check_value("explanation", ""), - check_type=eval(get_check_value("type", "False")), - default=get_check_value("default", NotImplemented), - match=get_check_value("regex", ""), - err_message=get_check_value("input_error", "Incorrect input"), - nmin=get_check_value("nmin", None), - nmax=get_check_value("nmax", None), - oob_error=get_check_value( - "oob_error", "Input out of bounds(Value too high/low/long/short)" - ), - options=get_check_value("options", None), - optional=get_check_value("optional", False), - ) + correct = False + + if not correct: + default_values = { + "explanation": "", + "var_type": "False", + "default": NotImplemented, + "regex": "", + "input_error": "Incorrect input", + "nmin": None, # noqa + "nmax": None, # noqa + "oob_error": "Input out of bounds(Value too high/low/long/short)", + "options": None, + "optional": False, + } + + [checks.update({key: value}) for key, value in default_values.items() if checks.get(key, 'Non') == 'Non'] + + value = handle_input(name=name, **checks) return value -def crawl_and_check(obj: dict, path: list, checks: dict = {}, name=""): - if len(path) == 0: - return check(obj, checks, name) - if path[0] not in obj.keys(): - obj[path[0]] = {} - obj[path[0]] = crawl_and_check(obj[path[0]], path[1:], checks, path[0]) +def nested_get( + obj: dict, + keys: list, +) -> any: + """ + Gets value from nested dict by list with path + + Args: + obj: Nested dict + keys: List with path + Return: + Value of last key + """ + for key in keys: + obj = obj.get(key, {}) return obj -def check_vars(path, checks): - global config - crawl_and_check(config, path, checks) +def nested_set( + obj: dict, + keys: list, + value: any, +) -> None: + """ + Sets last key in the nested dict by the path + + Args: + obj: Nested dict + keys: List with path + value: Value to set + """ + for key in keys[:-1]: + obj = obj.setdefault(key, {}) + obj[keys[-1]] = value + +def check_vars( + path: list, + checks: dict, +) -> None: + """ + Checks if value is in nested dict and correct by path of keys -def check_toml(template_file, config_file) -> Tuple[bool, Dict]: + Args: + path: List with path + checks: Dict with all checks + """ global config - config = None - try: - template = toml.load(template_file) - except Exception as error: - console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") - return False - try: - config = toml.load(config_file) - except toml.TomlDecodeError: - console.print( - f"""[blue]Couldn't read {config_file}. -Overwrite it?(y/n)""" - ) - if not input().startswith("y"): - print("Unable to read config, and not allowed to overwrite it. Giving up.") + if checks is None: + checks = dict() + + value = check( + nested_get(config, path), + checks, + name=path[-1], + ) + nested_set(config, path, value) + + +def check_config_wrapper( + func: function, +) -> function: + """ + Exception wrapper for check_config function + """ + + def wrapper(*args, **kwargs): + if args: + kwargs["name"] = args[0] + if args.__len__() > 1: + kwargs["template_name"] = args[-1] + if not kwargs or not all(arg is not None for arg in kwargs.values()): + kwargs["name"] = config_name + kwargs["template_name"] = config_template_name + + try: + return func(*args, **kwargs) + except toml.TomlDecodeError: + if console.input(f"[blue]Couldn't read {kwargs['name']}.\nOverwrite it?(y/n)").startswith("y"): + try: + with open(kwargs["name"], "w") as f: + f.write("") + return func(*args, **kwargs) + except Exception: # noqa (Exception is fine, it's not too broad) + console.print( + f"[red bold]Failed to overwrite {kwargs['name']}. Giving up.\n" + f"Suggestion: check {kwargs['name']} permissions for the user." + ) + return False + console.print("Unable to read config, and not allowed to overwrite it. Giving up.") return False - else: - try: - with open(config_file, "w") as f: - f.write("") - except: - console.print( - f"[red bold]Failed to overwrite {config_file}. Giving up.\nSuggestion: check {config_file} permissions for the user." - ) - return False - except FileNotFoundError: - console.print( - f"""[blue]Couldn't find {config_file} -Creating it now.""" - ) + # except Exception as error: + # console.print(f"[red bold]Encountered error when trying to to load {kwargs['template_name']}: {error}") + # return False + + return wrapper + + +@check_config_wrapper +def check_config( + name: Optional[str] = None, + template_name: Optional[str] = None, +) -> Union[dict, bool]: + """ + Checks config and returns corrected version + + Args: + name: Config name + template_name: Template name + Return: + Corrected config file as a dict + """ + if not name: + name = config_name + if not template_name: + template_name = config_template_name + + global config + + if not exists(name): + console.print(f"[blue]Couldn't find {name}\nCreating it now.") try: - with open(config_file, "x") as f: + with open(name, "w+") as f: f.write("") - config = {} - except: + config = dict() + except Exception: # noqa (Exception is fine, it's not too broad) console.print( - f"[red bold]Failed to write to {config_file}. Giving up.\nSuggestion: check the folder's permissions for the user." + f"[red bold]Failed to write to {name}.Giving up.\n" + f"Suggestion: check the folder's permissions for the user." ) return False + else: + config = toml.load(config_name) - console.print( - """\ -[blue bold]############################### -# # -# Checking TOML configuration # -# # -############################### -If you see any prompts, that means that you have unset/incorrectly set variables, please input the correct values.\ -""" - ) + template = toml.load(template_name) crawl(template, check_vars) - with open(config_file, "w") as f: + with open(config_name, "w") as f: toml.dump(config, f) return config if __name__ == "__main__": - check_toml("utils/.config.template.toml", "config.toml") + print(check_config()) + # template = toml.load(config_template_name) + # crawl(template)