Browse Source

ssync: put newly generated slides directly into obs using websocket

Noah Vogt 3 days ago
parent
commit
dcc340f85d

+ 2 - 2
README.md

@@ -115,12 +115,12 @@ Now for explanation of the individual entries.
 
 
 #### File Format and Naming
 #### File Format and Naming
 
 
-`IMAGE_FORMAT` forces a specific file format when writing the files in formats accepted by ImageMagick. The individual slides get named in this form: `${FILE_NAMEING}${SLIDE_NUMBER}${FILE_EXTENSION}`. Hence with the default config of
+`IMAGE_FORMAT` forces a specific file format when writing the files in formats accepted by ImageMagick. The individual slides get named in this form: `${FILE_NAMING}${SLIDE_NUMBER}${FILE_EXTENSION}`. Hence with the default config of
 
 
 ```python
 ```python
 IMAGE_FORMAT = "jpeg"
 IMAGE_FORMAT = "jpeg"
 FILE_EXTENSION = "jpg"
 FILE_EXTENSION = "jpg"
-FILE_NAMEING = "slide-"
+FILE_NAMING = "slide-"
 ```
 ```
 
 
 the slides would be named `slide-1.jpg`, `slide-2.jpg`, `slide-3.jpg` etc.
 the slides would be named `slide-1.jpg`, `slide-2.jpg`, `slide-3.jpg` etc.

+ 2 - 1
config/default_config.py

@@ -15,7 +15,7 @@
 
 
 IMAGE_FORMAT = "jpeg"
 IMAGE_FORMAT = "jpeg"
 FILE_EXTENSION = "jpg"
 FILE_EXTENSION = "jpg"
-FILE_NAMEING = "slide-"
+FILE_NAMING = "slide-"
 
 
 WIDTH = 1920
 WIDTH = 1920
 HEIGHT = 1080
 HEIGHT = 1080
@@ -81,6 +81,7 @@ SSYNC_CACHE_DIR = ""
 SSYNC_CHECKFILE_NAMING = "slidegen-checkfile.txt"
 SSYNC_CHECKFILE_NAMING = "slidegen-checkfile.txt"
 SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
 SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
 SSYNC_CHOSEN_FILE_NAMING = "chosen-file.txt"
 SSYNC_CHOSEN_FILE_NAMING = "chosen-file.txt"
+SSYNC_SLIDESHOW_INPUT_NAMING = "Songslides "
 
 
 OBS_SLIDES_DIR = ""
 OBS_SLIDES_DIR = ""
 OBS_SUBDIR_NAMING = ""
 OBS_SUBDIR_NAMING = ""

+ 102 - 10
input/slide_selection_iterator.py

@@ -14,6 +14,13 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import os
 import os
+from threading import Thread
+from pathlib import Path
+import contextlib
+import io
+import re
+
+import obsws_python as obs
 
 
 from utils import (
 from utils import (
     log,
     log,
@@ -40,6 +47,7 @@ def slide_selection_iterator(
     rclone_local_dir = expand_dir(const.RCLONE_LOCAL_DIR)
     rclone_local_dir = expand_dir(const.RCLONE_LOCAL_DIR)
 
 
     song_counter = 0
     song_counter = 0
+    threads = []
     while True:
     while True:
         song_counter += 1
         song_counter += 1
         input_prompt_prefix = "[{}{}] ".format(
         input_prompt_prefix = "[{}{}] ".format(
@@ -91,17 +99,101 @@ def slide_selection_iterator(
                 )
                 )
             )
             )
 
 
-            generate_slides_for_selected_song(
-                slide_style,
-                src_dir,
-                dest_dir,
-                generate_final_prompt(
-                    structure_prompt_answer, full_song_structure
-                ),
-                disable_async_enabled,
+            threads.extend(
+                generate_slides_for_selected_song(
+                    slide_style,
+                    src_dir,
+                    dest_dir,
+                    generate_final_prompt(
+                        structure_prompt_answer, full_song_structure
+                    ),
+                    disable_async_enabled,
+                )
             )
             )
 
 
+    log("waiting for subprocesses to finish ...")
+    for thread in threads:
+        if thread.is_alive():
+            thread.join()
+    log("subprocesses finished.")
+
     remove_chosenfile()
     remove_chosenfile()
+    add_slides_to_obs_slideshow_inputs()
+
+
+def add_slides_to_obs_slideshow_inputs():
+    pattern = re.compile(rf"{const.FILE_NAMING}(\d+)\.jpg$", re.IGNORECASE)
+
+    folders = []
+    for i in range(1, const.OBS_MIN_SUBDIRS + 1):
+        folders.append(
+            Path(const.OBS_SLIDES_DIR).joinpath(
+                Path(f"{const.OBS_SUBDIR_NAMING}{i}")
+            )
+        )
+    for folder in folders:
+        slides = []
+        for p in folder.iterdir():
+            m = pattern.match(p.name)
+            if m:
+                slides.append((int(m.group(1)), str(p.resolve())))
+        slides.sort(key=lambda x: x[0])
+        ordered_files = [{"value": path} for _, path in slides]
+
+        while True:
+            try:
+                # suppress stderr from obsws_python internals
+                with contextlib.redirect_stderr(io.StringIO()):
+                    cl = obs.ReqClient(
+                        host=const.OBS_WEBSOCKET_HOSTNAME,
+                        port=const.OBS_WEBSOCKET_PORT,
+                        password=const.OBS_WEBSOCKET_PASSWORD,
+                    )
+                source = (
+                    f"{const.SSYNC_SLIDESHOW_INPUT_NAMING}"
+                    + f"{str(folder.name)[len(const.OBS_SUBDIR_NAMING) :]}"
+                )
+
+                try:
+                    with contextlib.redirect_stderr(io.StringIO()):
+                        current_settings = cl.get_input_settings(
+                            source
+                        ).input_settings  # type: ignore
+
+                        new_settings = dict(current_settings)
+                        new_settings["files"] = ordered_files
+
+                        cl.set_input_settings(
+                            name=source,
+                            settings=new_settings,
+                            overlay=False,
+                        )
+
+                    log(f"{len(ordered_files)} slides put in " + f"'{source}'.")
+
+                    break
+
+                except obs.error.OBSSDKRequestError:  # type: ignore
+                    log(
+                        message=str(
+                            f"Error: Cannot access slideshow input: '{source}' "
+                            + "Please add to OBS and press enter to try again: "
+                        ),
+                        color="red",
+                        end="",
+                    )
+                    input()
+
+            except (ConnectionError, obs.error.OBSSDKError):  # type: ignore
+                log(
+                    message=str(
+                        "Error: Cannot connect to OBS Websocket. Please start OBS "
+                        + "and press enter to try again: "
+                    ),
+                    color="red",
+                    end="",
+                )
+                input()
 
 
 
 
 def generate_slides_for_selected_song(
 def generate_slides_for_selected_song(
@@ -110,14 +202,14 @@ def generate_slides_for_selected_song(
     dest_dir: str,
     dest_dir: str,
     calculated_prompt: str | list[str],
     calculated_prompt: str | list[str],
     disable_async_enabled: bool,
     disable_async_enabled: bool,
-) -> None:
+) -> list[Thread]:
     executing_slidegen_instance = slidegen.Slidegen(
     executing_slidegen_instance = slidegen.Slidegen(
         classic_slide_style,
         classic_slide_style,
         src_dir,
         src_dir,
         dest_dir,
         dest_dir,
         calculated_prompt,
         calculated_prompt,
     )
     )
-    executing_slidegen_instance.execute(disable_async_enabled)
+    return executing_slidegen_instance.execute(disable_async_enabled)
 
 
 
 
 def get_structure_for_prompt(classic_slide_style, src_dir, dest_dir):
 def get_structure_for_prompt(classic_slide_style, src_dir, dest_dir):

+ 3 - 0
input/validate_config.py

@@ -35,6 +35,9 @@ def validate_ssync_config() -> None:
         "OBS_SUBDIR_NAMING": const.OBS_SUBDIR_NAMING,
         "OBS_SUBDIR_NAMING": const.OBS_SUBDIR_NAMING,
         "OBS_MIN_SUBDIRS": const.OBS_MIN_SUBDIRS,
         "OBS_MIN_SUBDIRS": const.OBS_MIN_SUBDIRS,
         "SSYNC_CHOSEN_FILE_NAMING": const.SSYNC_CHOSEN_FILE_NAMING,
         "SSYNC_CHOSEN_FILE_NAMING": const.SSYNC_CHOSEN_FILE_NAMING,
+        "SSYNC_SLIDESHOW_INPUT_NAMING": const.SSYNC_SLIDESHOW_INPUT_NAMING,
+        "OBS_WEBSOCKET_HOSTNAME": const.OBS_WEBSOCKET_HOSTNAME,
+        "OBS_WEBSOCKET_PORT": const.OBS_WEBSOCKET_PORT,
     }
     }
     general_config_validator(needed_constants)
     general_config_validator(needed_constants)
 
 

+ 6 - 4
slidegen.py

@@ -15,6 +15,8 @@
 # You should have received a copy of the GNU General Public License
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+from threading import Thread
+
 import colorama
 import colorama
 
 
 from wand.image import Image
 from wand.image import Image
@@ -53,10 +55,10 @@ class Slidegen:
         self.chosen_structure = chosen_structure
         self.chosen_structure = chosen_structure
         self.slide_style: SlideStyle = slide_style
         self.slide_style: SlideStyle = slide_style
 
 
-    def execute(self, disable_async=False) -> None:
+    def execute(self, disable_async=False) -> list[Thread]:
         self.parse_file()
         self.parse_file()
         self.calculate_desired_structures()
         self.calculate_desired_structures()
-        self.generate_slides(disable_async)
+        return self.generate_slides(disable_async)
 
 
     def parse_file(self):
     def parse_file(self):
         parse_metadata(self)
         parse_metadata(self)
@@ -65,13 +67,13 @@ class Slidegen:
     def calculate_desired_structures(self) -> None:
     def calculate_desired_structures(self) -> None:
         self.chosen_structure = parse_prompt_input(self)
         self.chosen_structure = parse_prompt_input(self)
 
 
-    def generate_slides(self, disable_async: bool) -> None:
+    def generate_slides(self, disable_async: bool) -> list[Thread]:
         template_img: Image = generate_song_template(self)
         template_img: Image = generate_song_template(self)
 
 
         slide_count: int = count_number_of_slides_to_be_generated(self)
         slide_count: int = count_number_of_slides_to_be_generated(self)
         zfill_length: int = len(str(slide_count))
         zfill_length: int = len(str(slide_count))
 
 
-        generate_slides(
+        return generate_slides(
             self, slide_count, template_img, zfill_length, disable_async
             self, slide_count, template_img, zfill_length, disable_async
         )
         )
 
 

+ 6 - 4
slides/engine/generate_slides.py

@@ -37,7 +37,7 @@ def fix_timestamps(slidegen):
 
 
     folder = Path(slidegen.output_dir).resolve()
     folder = Path(slidegen.output_dir).resolve()
 
 
-    pattern = compile(rf"{const.FILE_NAMEING}(\d+)\.jpg$")
+    pattern = compile(rf"{const.FILE_NAMING}(\d+)\.jpg$")
 
 
     slides = []
     slides = []
     for f in folder.iterdir():
     for f in folder.iterdir():
@@ -60,7 +60,7 @@ def fix_timestamps(slidegen):
 
 
 def generate_slides(
 def generate_slides(
     slidegen, slide_count, template_img, zfill_length, disable_async: bool
     slidegen, slide_count, template_img, zfill_length, disable_async: bool
-) -> None:
+) -> list[Thread]:
     log("generating song slides...")
     log("generating song slides...")
 
 
     current_slide_index: int = 0
     current_slide_index: int = 0
@@ -148,6 +148,8 @@ def generate_slides(
     for thread in threads:
     for thread in threads:
         thread.start()
         thread.start()
 
 
+    return threads
+
 
 
 def generate_start_slide(slidegen, template_img, zfill_length, disable_async):
 def generate_start_slide(slidegen, template_img, zfill_length, disable_async):
     first_slide = slidegen.slide_style.start_slide_form()
     first_slide = slidegen.slide_style.start_slide_form()
@@ -162,7 +164,7 @@ def generate_start_slide(slidegen, template_img, zfill_length, disable_async):
         start_slide_img.save(
         start_slide_img.save(
             filename=path.join(
             filename=path.join(
                 slidegen.output_dir,
                 slidegen.output_dir,
-                const.FILE_NAMEING
+                const.FILE_NAMING
                 + "1".zfill(zfill_length)
                 + "1".zfill(zfill_length)
                 + "."
                 + "."
                 + const.FILE_EXTENSION,
                 + const.FILE_EXTENSION,
@@ -201,7 +203,7 @@ def generate_song_slide(
         song_slide_img.save(
         song_slide_img.save(
             filename=path.join(
             filename=path.join(
                 slidegen.output_dir,
                 slidegen.output_dir,
-                const.FILE_NAMEING
+                const.FILE_NAMING
                 + str(current_slide_index + 1).zfill(zfill_length)
                 + str(current_slide_index + 1).zfill(zfill_length)
                 + "."
                 + "."
                 + const.FILE_EXTENSION,
                 + const.FILE_EXTENSION,

+ 2 - 2
utils/log.py

@@ -50,8 +50,8 @@ def warn(message: str) -> None:
     print(colored("[*] Warning: {}".format(message), "yellow"))
     print(colored("[*] Warning: {}".format(message), "yellow"))
 
 
 
 
-def log(message: str, color="green") -> None:
-    print(colored("[*] {}".format(message), color))  # pyright: ignore
+def log(message: str, color="green", end="\n") -> None:
+    print(colored("[*] {}".format(message), color), end=end)  # pyright: ignore
 
 
 
 
 class CustomException(Exception):
 class CustomException(Exception):