Переглянути джерело

update docs + clearer ssync obs subdirectory naming + add support for env vars and ~ expansion + added path safety

Noah Vogt 2 роки тому
батько
коміт
d165d97d17

+ 12 - 10
README.md

@@ -1,6 +1,6 @@
 # slidegen
 
-As the name may partially imply, **`slidegen.py`** generates song slides as images - out of a plain text input file - intended for use with [OBS]() to livestream a typical (contemporary) Sunday church service.
+As the name may partially imply, **`slidegen.py`** generates song slides as images - out of a plain text input file - intended for use with [OBS](https://obsproject.com/) to livestream a typical (contemporary) Sunday church service.
 
 This program is also intended to be used in conjunction with **`ssync.py`**, which is basically a wrapper script that automatically syncs a local copy with the remote slide repository, removes the old obs slides and lets the user interactively choose the new slides with a smart fuzzy finder. It stands for slidesync, by the way.
 
@@ -98,6 +98,12 @@ TEXT_COLOR = "green"
 FILE_EXTENSION = "jpeg"
 ```
 
+Note that directories constants support environment variables in the form `$var` and `${var}` and the `~` user expansion. Also you can always use relative paths, so you don't really need to always enter the full absolute path. For example you could do something like this:
+
+```python
+OBS_SLIDES_DIR = "~/Documents/obs-${OBS_MAJOR_VERSION}/slides$month$weekday"
+```
+
 Now for explanation of the individual entries.
 
 #### File Format and Naming
@@ -263,13 +269,13 @@ RCLONE_REMOTE_DIR = 'mydr:"02 Liedtexte"'
 RCLONE_LOCAL_DIR = "/home/billy/Documents/songrepo"
 ```
 
-Here an example of how to setup the rclone variables. `RCLONE_REMOTE_DIR` sets the rclone remote directory in the typical rclone format and `RCLONE_LOCAL_DIR` is the local directory on your machine that rclone syncs to. For more information, please check the rclone documentation.
+Here an example of how to setup the rclone variables. `RCLONE_REMOTE_DIR` sets the rclone remote directory in the typical rclone format and `RCLONE_LOCAL_DIR` is the local directory on your machine that rclone syncs to. For more information, please check the [rclone documentation](https://rclone.org/docs/).
 
 
 #### SSync Cache
 
 ```python
-SSYNC_CACHE_DIR = "/home/billy/.cache/ssync"
+SSYNC_CACHE_DIR = "$XDG_CACHE_HOME/ssync"
 SSYNC_CHECKFILE_NAMING = "slidegen-checkfile.txt"
 SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
 ```
@@ -280,11 +286,11 @@ SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
 
 ```python
 OBS_MIN_SUBDIRS = 7
-OBS_SLIDES_DIR = "/home/billy/Documents/obs/slides"
-OBS_TARGET_SUBDIR = "Lied"
+OBS_SLIDES_DIR = "~/Documents/obs/slides"
+OBS_SUBDIR_NAMING = "Song "
 ```
 
-The slides are placed in subdirectories of `OBS_SLIDES_DIR` with the following naming: `${OBS_TARGET_SUBDIR} ${NUM}` with `NUM` being the number - also an integer - of the song selected by ssync, starting by 1. So that OBS doesn't complain about missing directories, empty directories are created following the naming for subdirectories up until the number defined by `OBS_MIN_SUBDIRS`.
+The slides are placed in subdirectories of `OBS_SLIDES_DIR` with the following naming: `${OBS_SUBDIR_NAMING}${NUM}` with `NUM` being the number - also an integer - of the song selected by ssync, starting by 1. So that OBS doesn't complain about missing directories, empty directories are created following the naming for subdirectories up until the number defined by `OBS_MIN_SUBDIRS`.
 
 ## Roadmap
 
@@ -293,15 +299,11 @@ These are some issues and possible changes that will be addressed or at least co
 - prevent all crashes:
     - safe `PROMPT_INPUT` parsing
     - handle possibly incorrect or insensible configurations safely
-    - ssync path safety
-- add support for environment variables and ~ symbol in (ssync) paths
-- clearer ssync obs subdirectory naming
 - asynchronous slide generation
 - 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 slidegen cause to terminate unsuccessfully
 - use a more typical commandline argument system
 - add more documentation, especially explaining the slide generation, but also dependencies and deployment
-- better handling of font path Configuration
 - add tests
 
 ## Licensing

+ 1 - 1
config/default_config.py

@@ -84,5 +84,5 @@ SSYNC_CHECKFILE_NAMING = "slidegen-checkfile.txt"
 SSYNC_CACHEFILE_NAMING = "slidegen-cachefile.txt"
 
 OBS_SLIDES_DIR = ""
-OBS_TARGET_SUBDIR = ""
+OBS_SUBDIR_NAMING = ""
 OBS_MIN_SUBDIRS = 7

+ 2 - 2
input/parse_argv.py

@@ -17,13 +17,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import sys
 
-from utils import log, error_msg
+from utils import log, error_msg, expand_dir
 
 
 def parse_argv_as_tuple() -> tuple:
     try:
         song_file_path = sys.argv[1]
-        output_dir = sys.argv[2]
+        output_dir = expand_dir(sys.argv[2])
     except IndexError:
         error_msg("incorrect amount of arguments provided, exiting...")
     try:

+ 19 - 10
input/slide_selection_iterator.py

@@ -17,7 +17,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 
-from utils import log, create_min_obs_subdirs
+from utils import log, create_min_obs_subdirs, error_msg, expand_dir
 from slides import ClassicSongTemplate, ClassicStartSlide, ClassicSongSlide
 
 import config as const
@@ -32,18 +32,27 @@ def slide_selection_iterator():
         + " eg. [1,R,2,R] / [1-4]: "
     )
     file_list_str = ""
-    for file in os.listdir(const.RCLONE_LOCAL_DIR):
-        file_list_str += file + "\n"
+    rclone_local_dir = expand_dir(const.RCLONE_LOCAL_DIR)
+    obs_slides_dir = expand_dir(const.OBS_SLIDES_DIR)
+    try:
+        for file in os.listdir(rclone_local_dir):
+            file_list_str += file + "\n"
+    except (FileNotFoundError, PermissionError, IOError) as error:
+        error_msg(
+            "Failed to access items in '{}'. Reason: {}".format(
+                rclone_local_dir, error
+            )
+        )
     file_list_str = file_list_str[:-1]
     tempfile_str = ".chosen-tempfile"
 
     index = 0
     while True:
         index += 1
-        input_song_prompt = "[{} {}] ".format(const.OBS_TARGET_SUBDIR, index)
+        input_song_prompt = "[{}{}] ".format(const.OBS_SUBDIR_NAMING, index)
         prompt_answer = str(input(input_song_prompt + iterator_prompt))
         if prompt_answer.lower() == "y":
-            create_min_obs_subdirs(index)
+            create_min_obs_subdirs()
             break
 
         file_list_str = file_list_str.replace("\n", "\\n")
@@ -62,14 +71,14 @@ def slide_selection_iterator():
             )
 
             log(
-                "generating slides '{}' to '{} {}'...".format(
-                    chosen_song_file, const.OBS_TARGET_SUBDIR, index
+                "generating slides '{}' to '{}{}'...".format(
+                    chosen_song_file, const.OBS_SUBDIR_NAMING, index
                 )
             )
-            src_dir = os.path.join(const.RCLONE_LOCAL_DIR, chosen_song_file)
+            src_dir = os.path.join(rclone_local_dir, chosen_song_file)
             dest_dir = os.path.join(
-                const.OBS_SLIDES_DIR,
-                const.OBS_TARGET_SUBDIR + " " + str(index),
+                obs_slides_dir,
+                const.OBS_SUBDIR_NAMING + str(index),
             )
             os.mkdir(dest_dir)
 

+ 1 - 1
input/validate_ssync_config.py

@@ -28,7 +28,7 @@ def validate_ssync_config():
         "SSYNC_CACHEFILE_NAMING": const.SSYNC_CACHEFILE_NAMING,
         "SSYNC_CACHE_DIR": const.SSYNC_CACHE_DIR,
         "OBS_SLIDES_DIR": const.OBS_SLIDES_DIR,
-        "OBS_TARGET_SUBDIR": const.OBS_TARGET_SUBDIR,
+        "OBS_TARGET_SUBDIR": const.OBS_SUBDIR_NAMING,
         "OBS_MIN_SUBDIRS": const.OBS_MIN_SUBDIRS,
     }
     for key in needed_constants:

+ 11 - 9
sync/save_new_checkfile.py

@@ -19,23 +19,25 @@ from os import system, path
 
 import shutil
 
-from utils import log
+from utils import log, expand_dir, error_msg
 
 import config as const
 
 
 def save_new_checkfile() -> None:
+    cache_dir = expand_dir(const.SSYNC_CACHE_DIR)
     log("saving new checkfile...")
     system(
         'rclone md5sum {} > "{}"'.format(
             const.RCLONE_REMOTE_DIR,
-            path.join(const.SSYNC_CACHE_DIR, const.SSYNC_CHECKFILE_NAMING),
+            path.join(cache_dir, const.SSYNC_CHECKFILE_NAMING),
         )
     )
-    if not path.isfile(
-        path.join(const.SSYNC_CACHE_DIR, const.SSYNC_CACHEFILE_NAMING)
-    ):
-        shutil.copyfile(
-            path.join(const.SSYNC_CACHE_DIR, const.SSYNC_CHECKFILE_NAMING),
-            path.join(const.SSYNC_CACHE_DIR, const.SSYNC_CACHEFILE_NAMING),
-        )
+    try:
+        if not path.isfile(path.join(cache_dir, const.SSYNC_CACHEFILE_NAMING)):
+            shutil.copyfile(
+                path.join(cache_dir, const.SSYNC_CHECKFILE_NAMING),
+                path.join(cache_dir, const.SSYNC_CACHEFILE_NAMING),
+            )
+    except (FileNotFoundError, PermissionError, IOError) as error:
+        error_msg("Failed to save new checkfile. Reason: {}".format(error))

+ 1 - 0
utils/__init__.py

@@ -25,3 +25,4 @@ from .strings import (
 from .img import get_empty_image
 from .create_min_obs_subdirs import create_min_obs_subdirs
 from .clear_obs_slides_dir import clear_obs_slides_dir
+from .path import expand_dir

+ 18 - 11
utils/clear_obs_slides_dir.py

@@ -18,19 +18,26 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import os
 import shutil
 
-from utils import log, error_msg
-
 import config as const
 
+from .log import log, error_msg
+from .path import expand_dir
+
 
 def clear_obs_slides_dir() -> None:
     log("clearing obs slides directory...")
-    for filename in os.listdir(const.OBS_SLIDES_DIR):
-        file_path = os.path.join(const.OBS_SLIDES_DIR, filename)
-        try:
-            if os.path.isfile(file_path) or os.path.islink(file_path):
-                os.unlink(file_path)
-            elif os.path.isdir(file_path):
-                shutil.rmtree(file_path)
-        except IOError as error:
-            error_msg("Failed to delete %s. Reason: %s" % (file_path, error))
+    expanded_dir = expand_dir(const.OBS_SLIDES_DIR)
+    try:
+        for filename in os.listdir(expanded_dir):
+            file_path = os.path.join(expanded_dir, filename)
+            try:
+                if os.path.isfile(file_path) or os.path.islink(file_path):
+                    os.unlink(file_path)
+                elif os.path.isdir(file_path):
+                    shutil.rmtree(file_path)
+            except (FileNotFoundError, PermissionError, IOError) as error:
+                error_msg("Failed to delete %s. Reason: %s" % (file_path, error))
+    except (FileNotFoundError, PermissionError, IOError) as error:
+        error_msg("could not list directory '{}'. Reason: {}".format(
+            expanded_dir, error
+                    ))

+ 35 - 10
utils/create_min_obs_subdirs.py

@@ -19,14 +19,39 @@ import os
 
 import config as const
 
-
-def create_min_obs_subdirs(count: int) -> None:
-    if count >= const.OBS_MIN_SUBDIRS:
-        return
-
-    for number in range(count, const.OBS_MIN_SUBDIRS + 1):
-        dirname = os.path.join(
-            const.OBS_SLIDES_DIR,
-            const.OBS_TARGET_SUBDIR + " " + str(number),
+from .log import error_msg, log
+from .path import expand_dir
+
+
+def create_min_obs_subdirs() -> None:
+    obs_slides_dir = expand_dir(const.OBS_SLIDES_DIR)
+
+    subdirs_to_create = []
+    for num in range(1, const.OBS_MIN_SUBDIRS + 1):
+        subdirs_to_create.append(num)
+    for file in os.listdir(obs_slides_dir):
+        if file.startswith(str(const.OBS_SUBDIR_NAMING)):
+            try:
+                index = int(file[len(str(const.OBS_SUBDIR_NAMING)) :])
+            except ValueError:
+                error_msg(
+                    "could not parse file '{}' in '{}'".format(
+                        file, obs_slides_dir
+                    )
+                )
+            if index in subdirs_to_create:
+                subdirs_to_create.remove(index)
+
+    dirname = ""
+    try:
+        for number in subdirs_to_create:
+            dirname = os.path.join(
+                obs_slides_dir,
+                const.OBS_SUBDIR_NAMING + str(number),
+            )
+            os.mkdir(dirname)
+            log("creating empty slide directory '{}'...".format(dirname))
+    except (FileNotFoundError, PermissionError, IOError) as error:
+        error_msg(
+            "Failed to create directory '{}'. Reason: {}".format(dirname, error)
         )
-        os.mkdir(dirname)

+ 25 - 0
utils/path.py

@@ -0,0 +1,25 @@
+"""
+Copyright © 2022 Noah Vogt <noah@noahvogt.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import os
+
+
+def expand_dir(directory: str) -> str:
+    expanded_user_dir = os.path.expanduser(directory)
+    expanded_user_and_env_vars_dir = os.path.expandvars(expanded_user_dir)
+    abs_path = os.path.abspath(expanded_user_and_env_vars_dir)
+    return abs_path