瀏覽代碼

implement safe prompt input parsing

Noah Vogt 2 年之前
父節點
當前提交
6bd9335adb
共有 6 個文件被更改,包括 148 次插入64 次删除
  1. 3 6
      README.md
  2. 1 0
      config/default_config.py
  3. 1 1
      input/__init__.py
  4. 75 39
      input/parse_prompt.py
  5. 67 18
      input/slide_selection_iterator.py
  6. 1 0
      input/validate_config.py

+ 3 - 6
README.md

@@ -305,9 +305,10 @@ Here an example of how to setup the rclone variables. `RCLONE_REMOTE_DIR` sets t
 SSYNC_CACHE_DIR = "$XDG_CACHE_HOME/ssync"
 SSYNC_CHECKFILE_NAMING = "slidegen-checkfile.txt"
 SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
+SSYNC_CHOSEN_FILE_NAMING = ".chosen-file.txt"
 ```
 
-`SSYNC_CACHE_DIR` sets the directory in which the checkfile and cachefile of `ssync.py` get placed. You can change their name by setting `SSYNC_CACHEFILE_NAMING` for the cachefile and `SSYNC_CHECKFILE_NAMING` for the checkfile.
+`SSYNC_CACHE_DIR` sets the directory in which the checkfile and cachefile of `ssync.py` get placed. You can change their name by setting `SSYNC_CACHEFILE_NAMING` for the cachefile and `SSYNC_CHECKFILE_NAMING` for the checkfile. Same for the cache file that stores the chosen prompt fzf answer in `SSYNC_CHOSEN_FILE_NAMING`.
 
 #### OBS Slide Settings
 
@@ -323,11 +324,7 @@ The slides are placed in subdirectories of `OBS_SLIDES_DIR` with the following n
 
 These are some issues and possible changes that will be addressed or at least considered by our future development efforts:
 
-- prevent all crashes:
-    - safe `PROMPT_INPUT` parsing
-- use caching, with checksum checks for changes in the source file and the `PROMPT_INPUT`
-- provide ssync with the song structure, display it to the user and prevent him from entering a prompt that would cause slidegen to terminate unsuccessfully
-- add more documentation, especially explaining the slide generation, but also dependencies and deployment
+- add more documentation, especially explaining the slide generation, but also dependencies and deployment and the `PROMPT_INPUT`
 - add tests
 - use smarter multi slide splitter algorithm: either by pattern recognition like line matching or rhymes of the last word or by incorporating some sort of sub-song-structures in the body.
 

+ 1 - 0
config/default_config.py

@@ -82,6 +82,7 @@ RCLONE_LOCAL_DIR = ""
 SSYNC_CACHE_DIR = ""
 SSYNC_CHECKFILE_NAMING = "slidegen-checkfile.txt"
 SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
+SSYNC_CHOSEN_FILE_NAMING = ".chosen-file.txt"
 
 OBS_SLIDES_DIR = ""
 OBS_SUBDIR_NAMING = ""

+ 1 - 1
input/__init__.py

@@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
 
-from .parse_prompt import parse_prompt_input
+from .parse_prompt import parse_prompt_input, generate_final_prompt
 from .parse_file import (
     parse_metadata,
     parse_songtext,

+ 75 - 39
input/parse_prompt.py

@@ -15,56 +15,92 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
 
+from re import match
+
 from utils import (
     log,
-    error_msg,
     structure_as_list,
+    get_unique_structure_elements,
 )
 
 
 def parse_prompt_input(slidegen) -> list:
     full_structure_list = structure_as_list(slidegen.metadata["structure"])
-    if len(slidegen.chosen_structure) == 0:
-        log(
-            "chosen structure: {}".format(str(slidegen.metadata["structure"])),
-            color="cyan",
-        )
-        return structure_as_list(slidegen.metadata["structure"])
-    if not "-" in slidegen.chosen_structure:
+    calculated_prompt = generate_final_prompt(
+        str(slidegen.chosen_structure), slidegen.metadata["structure"]
+    )
+    log(
+        "chosen structure: {}".format(calculated_prompt),
+        color="cyan",
+    )
+    return structure_as_list(calculated_prompt)
+
+
+def generate_final_prompt(structure_prompt_answer, full_song_structure) -> str:
+    valid_prompt, calculated_prompt = is_and_give_prompt_input_valid(
+        structure_prompt_answer, full_song_structure
+    )
+
+    if not valid_prompt:
         log(
-            "chosen structure: {}".format(str(slidegen.chosen_structure)),
-            color="cyan",
+            "warning: prompt input '{}' is invalid, defaulting to full song structure".format(
+                structure_prompt_answer
+            ),
+            color="yellow",
         )
-        return structure_as_list(str(slidegen.chosen_structure))
-
-    dash_index = str(slidegen.chosen_structure).find("-")
-    start_verse = str(slidegen.chosen_structure[:dash_index]).strip()
-    end_verse = str(slidegen.chosen_structure[dash_index + 1 :]).strip()
-
-    try:
-        if int(start_verse) >= int(end_verse):
-            error_msg("{} < {} must be true".format(start_verse, end_verse))
-        if start_verse not in slidegen.metadata["structure"]:
-            error_msg("structure {} unknown".format(start_verse))
-        if end_verse not in slidegen.metadata["structure"]:
-            error_msg("structure {} unknown".format(end_verse))
-    except (ValueError, IndexError):
-        error_msg("please choose a valid integer for the song structure")
-
-    start_index = full_structure_list.index(start_verse)
+        calculated_prompt = full_song_structure
+    return calculated_prompt
+
+
+def is_and_give_prompt_input_valid(
+    prompt: str, full_structure: list
+) -> tuple[bool, str]:
+    if not match(
+        r"^(([0-9]+|R)|[0-9]+-[0-9]+)(,(([0-9]+|R)|[0-9]+-[0-9]+))*$", prompt
+    ):
+        return False, ""
+
+    allowed_elements = get_unique_structure_elements(full_structure)
+    test_elements = prompt.split(",")
+    print("test elemets before loops: {}".format(test_elements))
+    for index, element in enumerate(test_elements):
+        if "-" in element:
+            splitted_dashpart = element.split("-")
+            if splitted_dashpart[0] >= splitted_dashpart[1]:
+                return False, ""
+            if splitted_dashpart[0] not in allowed_elements:
+                return False, ""
+            if splitted_dashpart[1] not in allowed_elements:
+                return False, ""
+
+            dotted_part = calculate_dashed_prompt_part(
+                full_structure, splitted_dashpart[0], splitted_dashpart[1]
+            )
+            dotted_part.reverse()
+            test_elements[index] = dotted_part[0]
+            for left_over_dotted_part_element in dotted_part[1:]:
+                test_elements.insert(index, left_over_dotted_part_element)
+        else:
+            if element not in allowed_elements:
+                return False, ""
+
+    return True, ",".join(test_elements)
+
+
+def calculate_dashed_prompt_part(
+    content: list, start_verse: str, end_verse: str
+) -> list:
+    content = list(content)
+    for i in content:
+        if i == ",":
+            content.remove(i)
+    start_index = content.index(start_verse)
     if start_index != 0:
-        if (
-            full_structure_list[0] == "R"
-            and full_structure_list[start_index - 1] == "R"
-        ):
+        if content[0] == "R" and content[start_index - 1] == "R":
             start_index -= 1
-    end_index = full_structure_list.index(end_verse)
-    if end_index != len(full_structure_list) - 1:
-        if (
-            full_structure_list[-1] == "R"
-            and full_structure_list[end_index + 1] == "R"
-        ):
+    end_index = content.index(end_verse)
+    if end_index != len(content) - 1:
+        if content[-1] == "R" and content[end_index + 1] == "R":
             end_index += 1
 
-    log("chosen structure: {}".format(str(slidegen.chosen_structure)))
-    return full_structure_list[start_index : end_index + 1]
+    return content[start_index : end_index + 1]

+ 67 - 18
input/slide_selection_iterator.py

@@ -17,8 +17,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 
-from utils import log, create_min_obs_subdirs, error_msg, expand_dir
+from utils import (
+    log,
+    create_min_obs_subdirs,
+    error_msg,
+    expand_dir,
+)
 from slides import ClassicSongTemplate, ClassicStartSlide, ClassicSongSlide
+from input import parse_metadata, generate_final_prompt
 
 import config as const
 
@@ -44,7 +50,7 @@ def slide_selection_iterator(ssync):
             )
         )
     file_list_str = file_list_str[:-1]
-    tempfile_str = ".chosen-tempfile"
+    const.SSYNC_CHOSEN_FILE_NAMING = ".chosen-tempfile"
 
     index = 0
     while True:
@@ -56,18 +62,43 @@ def slide_selection_iterator(ssync):
             break
 
         file_list_str = file_list_str.replace("\n", "\\n")
-        os.system('printf "{}" | fzf > {}'.format(file_list_str, tempfile_str))
+        os.system(
+            'printf "{}" | fzf > {}'.format(
+                file_list_str, const.SSYNC_CHOSEN_FILE_NAMING
+            )
+        )
 
-        with open(
-            tempfile_str, encoding="utf-8", mode="r"
-        ) as tempfile_file_opener:
-            chosen_song_file = tempfile_file_opener.read()[:-1].strip()
+        chosen_song_file = read_chosen_song_file()
 
         if len(chosen_song_file) == 0:
             log("no slides chosen, skipping...")
         else:
+            src_dir = os.path.join(rclone_local_dir, chosen_song_file)
+            dest_dir = create_and_get_dest_dir(obs_slides_dir, index)
+
+            dummy_slidegen_instance = slidegen.Slidegen(
+                ClassicSongTemplate,
+                ClassicStartSlide,
+                ClassicSongSlide,
+                src_dir,
+                dest_dir,
+                "",
+            )
+            parse_metadata(dummy_slidegen_instance)
+            full_song_structure = dummy_slidegen_instance.metadata["structure"]
+            log(
+                "full song structure of '{}':\n{}".format(
+                    chosen_song_file,
+                    full_song_structure,
+                ),
+                color="magenta",
+            )
+
             structure_prompt_answer = input(
                 input_song_prompt + structure_prompt
+            ).strip()
+            calculated_prompt = generate_final_prompt(
+                structure_prompt_answer, full_song_structure
             )
 
             log(
@@ -75,22 +106,40 @@ def slide_selection_iterator(ssync):
                     chosen_song_file, const.OBS_SUBDIR_NAMING, index
                 )
             )
-            src_dir = os.path.join(rclone_local_dir, chosen_song_file)
-            dest_dir = os.path.join(
-                obs_slides_dir,
-                const.OBS_SUBDIR_NAMING + str(index),
-            )
-            os.mkdir(dest_dir)
 
-            slidegen_instance = slidegen.Slidegen(
+            executing_slidegen_instance = slidegen.Slidegen(
                 ClassicSongTemplate,
                 ClassicStartSlide,
                 ClassicSongSlide,
                 src_dir,
                 dest_dir,
-                structure_prompt_answer,
+                calculated_prompt,
             )
-            slidegen_instance.execute(ssync.disable_async)
+            executing_slidegen_instance.execute(ssync.disable_async)
+
+    remove_chosenfile()
+
+
+def remove_chosenfile() -> None:
+    try:
+        if os.path.isfile(const.SSYNC_CHOSEN_FILE_NAMING):
+            os.remove(const.SSYNC_CHOSEN_FILE_NAMING)
+    except (FileNotFoundError, PermissionError, IOError) as error:
+        error_msg("Failed to remove chosenfile. Reason: {}".format(error))
+
+
+def create_and_get_dest_dir(obs_slides_dir, index) -> str:
+    dest_dir = os.path.join(
+        obs_slides_dir,
+        const.OBS_SUBDIR_NAMING + str(index),
+    )
+    os.mkdir(dest_dir)
+    return dest_dir
+
 
-    if os.path.isfile(tempfile_str):
-        os.remove(tempfile_str)
+def read_chosen_song_file() -> str:
+    with open(
+        const.SSYNC_CHOSEN_FILE_NAMING, encoding="utf-8", mode="r"
+    ) as tempfile_file_opener:
+        chosen_song_file = tempfile_file_opener.read()[:-1].strip()
+    return chosen_song_file

+ 1 - 0
input/validate_config.py

@@ -30,6 +30,7 @@ def validate_ssync_config() -> None:
         "OBS_SLIDES_DIR": const.OBS_SLIDES_DIR,
         "OBS_SUBDIR_NAMING": const.OBS_SUBDIR_NAMING,
         "OBS_MIN_SUBDIRS": const.OBS_MIN_SUBDIRS,
+        "SSYNC_CHOSEN_FILE_NAMING": const.SSYNC_CHOSEN_FILE_NAMING,
     }
     general_config_validator(needed_constants)