Просмотр исходного кода

started working on sermon upload

Noah Vogt 1 год назад
Родитель
Сommit
cd5c644966
11 измененных файлов с 372 добавлено и 57 удалено
  1. 1 36
      choose_cd_dialog.py
  2. 44 0
      choose_sermon_dialog.py
  3. 5 0
      config/default_config.py
  4. 1 0
      input/__init__.py
  5. 2 19
      input/gui.py
  6. 12 0
      input/validate_config.py
  7. 33 0
      upload_sermon_of_today.py
  8. 9 1
      utils/__init__.py
  9. 40 0
      utils/audio.py
  10. 5 1
      utils/log.py
  11. 220 0
      utils/scripts.py

+ 1 - 36
choose_cd_dialog.py

@@ -17,53 +17,18 @@ 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 sys
-from os import listdir
-
 from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
-    QApplication,
-    QDialog,
     QMessageBox,
 )
 
 from utils import (
     burn_cds_of_day,
-    log,
+    choose_cd_day,
 )
 from input import (
     validate_cd_record_config,
-    RadioButtonDialog,
     InfoMsgBox,
 )
-import config as const
-
-
-def choose_cd_day() -> list[str]:
-    # pylint: disable=unused-variable
-    app = QApplication([])
-    try:
-        dirs = sorted(listdir(const.CD_RECORD_OUTPUT_BASEDIR))
-        dirs.reverse()
-
-        if not dirs:
-            return [
-                f"Did not find any CD's in: {const.CD_RECORD_OUTPUT_BASEDIR}.",
-                "",
-            ]
-
-        dialog = RadioButtonDialog(dirs, "Choose a CD to Burn")
-        if dialog.exec_() == QDialog.Accepted:
-            # TODO: update text for possibly mutliple CD's
-            log(f"Burning CD for day: {dialog.chosen}")
-            return ["", dialog.chosen]
-        return ["ignore", ""]
-    except (FileNotFoundError, PermissionError, IOError):
-        pass
-
-    return [
-        f"Failed to access directory: {const.CD_RECORD_OUTPUT_BASEDIR}.",
-        "",
-    ]
 
 
 def choose_and_burn_cd():

+ 44 - 0
choose_sermon_dialog.py

@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+
+"""
+Copyright © 2024 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/>.
+"""
+
+from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
+    QMessageBox,
+)
+
+from utils import (
+    upload_sermon_for_day,
+    choose_sermon_day,
+)
+from input import (
+    validate_sermon_upload_config,
+    InfoMsgBox,
+)
+
+
+def choose_and_upload_sermon():
+    msg, yyyy_mm_dd = choose_sermon_day()
+    if msg == "":
+        upload_sermon_for_day(yyyy_mm_dd)
+    elif msg != "ignore":
+        InfoMsgBox(QMessageBox.Critical, "Error", msg)
+
+
+if __name__ == "__main__":
+    validate_sermon_upload_config()
+    choose_and_upload_sermon()

+ 5 - 0
config/default_config.py

@@ -100,3 +100,8 @@ CD_RECORD_FFMPEG_INPUT_ARGS = ""
 CD_RECORD_MAX_SECONDS = 4800
 CD_RECORD_MIN_TRACK_MILIS = 4200
 CD_RECORD_PREFERED_DRIVE = ""
+
+SERMON_UPLOAD_FTP_HOSTNAME = ""
+SERMON_UPLOAD_FTP_USER = ""
+SERMON_UPLOAD_FTP_PASSWORD = ""
+SERMON_UPLOAD_FTP_UPLOAD_DIR = ""

+ 1 - 0
input/__init__.py

@@ -30,6 +30,7 @@ from .validate_config import (
     validate_ssync_config,
     validate_obs_song_scene_switcher_config,
     validate_cd_record_config,
+    validate_sermon_upload_config,
 )
 from .slide_selection_iterator import slide_selection_iterator
 from .gui import RadioButtonDialog, InfoMsgBox, SheetAndPreviewChooser

+ 2 - 19
input/gui.py

@@ -16,12 +16,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
 
 import sys
-from os import listdir, path
+from os import path
 from re import match
 from dataclasses import dataclass
 
-from soundfile import SoundFile, LibsndfileError  # pyright: ignore
-
 from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
     QApplication,
     QDialog,
@@ -47,9 +45,7 @@ from PyQt5.QtCore import (  # pylint: disable=no-name-in-module
     QTimer,
 )
 
-
-class CustomException(Exception):
-    pass
+from utils import CustomException, get_wave_duration_in_secs
 
 
 @dataclass
@@ -132,19 +128,6 @@ class RadioButtonDialog(QDialog):  # pylint: disable=too-few-public-methods
             )
 
 
-def get_wave_duration_in_secs(file_name: str) -> int:
-    try:
-        wav = SoundFile(file_name)
-        return int(wav.frames / wav.samplerate)
-    except LibsndfileError:
-        QMessageBox.critical(
-            None,
-            "Error",
-            f"Error: Could not get duration of {file_name}",
-        )
-        sys.exit(1)
-
-
 class SheetAndPreviewChooser(QDialog):  # pylint: disable=too-few-public-methods
     def __init__(
         self, base_dir: str, options: list[str], window_title: str

+ 12 - 0
input/validate_config.py

@@ -56,6 +56,18 @@ def validate_cd_record_config() -> None:
     general_config_validator(needed_constants)
 
 
+def validate_sermon_upload_config() -> None:
+    needed_constants: dict = {
+        "CD_RECORD_CACHEFILE": const.CD_RECORD_CACHEFILE,
+        "CD_RECORD_OUTPUT_BASEDIR": const.CD_RECORD_OUTPUT_BASEDIR,
+        "SERMON_UPLOAD_FTP_HOSTNAME": const.SERMON_UPLOAD_FTP_HOSTNAME,
+        "SERMON_UPLOAD_FTP_USER": const.SERMON_UPLOAD_FTP_USER,
+        "SERMON_UPLOAD_FTP_PASSWORD": const.SERMON_UPLOAD_FTP_PASSWORD,
+        "SERMON_UPLOAD_FTP_UPLOAD_DIR": const.SERMON_UPLOAD_FTP_UPLOAD_DIR,
+    }
+    general_config_validator(needed_constants)
+
+
 def general_config_validator(needed_constants: dict) -> None:
     for key in needed_constants:
         if needed_constants.get(key) == "":

+ 33 - 0
upload_sermon_of_today.py

@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+"""
+Copyright © 2024 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/>.
+"""
+
+from utils import (
+    make_sure_there_is_no_ongoing_cd_recording,
+    get_yyyy_mm_dd_date,
+    upload_sermon_for_day,
+)
+from input import (
+    validate_sermon_upload_config,
+)
+
+
+if __name__ == "__main__":
+    validate_sermon_upload_config()
+    make_sure_there_is_no_ongoing_cd_recording()
+    upload_sermon_for_day(get_yyyy_mm_dd_date())

+ 9 - 1
utils/__init__.py

@@ -15,7 +15,8 @@ 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 .log import error_msg, warn, log
+from .audio import get_wave_duration_in_secs, get_wave_duration_in_frames
+from .log import error_msg, warn, log, CustomException
 from .strings import (
     get_songtext_by_structure,
     structure_as_list,
@@ -34,4 +35,11 @@ from .scripts import (
     mark_end_of_recording,
     cycle_to_song_direction,
     SongDirection,
+    make_sure_there_is_no_ongoing_cd_recording,
+    get_possible_sermon_segments_of_day,
+    get_segments_over_20_mins,
+    upload_sermon_segment,
+    choose_cd_day,
+    choose_sermon_day,
+    upload_sermon_for_day,
 )

+ 40 - 0
utils/audio.py

@@ -0,0 +1,40 @@
+"""
+Copyright © 2024 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 sys
+
+from soundfile import SoundFile, LibsndfileError  # pyright: ignore
+from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
+    QMessageBox,
+)
+
+
+def get_wave_duration_in_frames(file_name: str) -> int:
+    try:
+        wav = SoundFile(file_name)
+        return int(wav.frames / wav.samplerate * 75)
+    except LibsndfileError:
+        QMessageBox.critical(
+            None,
+            "Error",
+            f"Error: Could not get duration of {file_name}",
+        )
+        sys.exit(1)
+
+
+def get_wave_duration_in_secs(file_name: str) -> int:
+    return int(get_wave_duration_in_frames(file_name) / 75)

+ 5 - 1
utils/log.py

@@ -30,4 +30,8 @@ def warn(message: str) -> None:
 
 
 def log(message: str, color="green") -> None:
-    print(colored("[*] {}".format(message), color))
+    print(colored("[*] {}".format(message), color))  # pyright: ignore
+
+
+class CustomException(Exception):
+    pass

+ 220 - 0
utils/scripts.py

@@ -22,11 +22,14 @@ from shlex import split
 from time import sleep
 from re import match
 from enum import Enum
+from dataclasses import dataclass
+import ftplib
 
 from pyautogui import keyDown, keyUp
 from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
     QApplication,
     QMessageBox,
+    QInputDialog,
 )
 from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
     QDialog,
@@ -38,6 +41,8 @@ from utils import (
     error_msg,
     get_yyyy_mm_dd_date,
     expand_dir,
+    CustomException,
+    get_wave_duration_in_frames,
 )
 from input import (
     validate_cd_record_config,
@@ -351,3 +356,218 @@ def burn_and_eject_cd(
         InfoMsgBox(QMessageBox.Critical, "Error", "Error: Failed to burn CD.")
 
     eject_drive(drive)
+
+
+def make_sure_there_is_no_ongoing_cd_recording() -> None:
+    if path.isfile(const.CD_RECORD_CACHEFILE):
+        cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
+        if is_valid_cd_record_checkfile(
+            cachefile_content, get_yyyy_mm_dd_date()
+        ):
+            if cachefile_content[1].strip() != "9001":
+                InfoMsgBox(
+                    QMessageBox.Critical,
+                    "Error",
+                    "Error: Ongoing CD Recording detected",
+                )
+                sys.exit(1)
+
+
+def get_index_line_as_frames(line: str) -> int:
+    stripped_line = line.strip()
+    frames = 75 * 60 * int(stripped_line[9:11])
+    frames += 75 * int(stripped_line[12:14])
+    frames += int(stripped_line[15:17])
+    return frames
+
+
+@dataclass
+class SermonSegment:
+    start_frame: int
+    end_frame: int
+    source_cue_sheet: str
+    source_marker: int
+
+
+def get_segments_over_20_mins(
+    segments: list[SermonSegment],
+) -> list[SermonSegment]:
+    suitable_segments = []
+    for segment in segments:
+        if segment.end_frame - segment.start_frame >= 2250:  # 75 * 60 * 20
+            # if segment.end_frame - segment.start_frame >= 90000:  # 75 * 60 * 20
+            suitable_segments.append(segment)
+    return suitable_segments
+
+
+def get_possible_sermon_segments_of_day(yyyy_mm_dd: str) -> list[SermonSegment]:
+    try:
+        segments = []
+        base_frames = 0
+        max_frames = 0
+
+        day_dir = path.join(const.CD_RECORD_OUTPUT_BASEDIR, yyyy_mm_dd)
+        files = sorted(listdir(day_dir))
+        cue_sheets = []
+        for file in files:
+            if is_legal_sheet_filename(file):
+                cue_sheets.append(file)
+        for sheet_num, sheet in enumerate(cue_sheets):
+            with open(
+                path.join(day_dir, sheet),
+                mode="r",
+                encoding="utf-8-sig",
+            ) as sheet_reader:
+                sheet_content = sheet_reader.readlines()
+            start_frame = 0
+            end_frame = 0
+            wav_path = ""
+            max_line_num = 0
+            for line_num, line in enumerate(sheet_content):
+                max_line_num = line_num
+                if line_num == 0:
+                    if not match(r"^FILE \".+\" WAVE$", line):
+                        raise CustomException("invalid first cue sheet line")
+                    wav_path = line[line.find('"') + 1 :]
+                    wav_path = wav_path[: wav_path.rfind('"')]
+                elif match(r"^\s+INDEX 01 ([0-9]{2}:){2}[0-9]{2}\s*$", line):
+                    if line_num != 2:
+                        end_frame = get_index_line_as_frames(line)
+                        segments.append(
+                            SermonSegment(
+                                start_frame,
+                                end_frame,
+                                sheet,
+                                (max_line_num - 2) // 2,
+                            )
+                        )
+                        start_frame = end_frame
+
+            segments.append(
+                SermonSegment(
+                    start_frame,
+                    get_wave_duration_in_frames(wav_path),
+                    sheet,
+                    max_line_num // 2,
+                )
+            )
+            # for segment in file_segments:
+            #     log(f"start {segment.start_frame}")
+            #     log(f"end {segment.end_frame}")
+            #     log(f"sheet {segment.source_cue_sheet}")
+            #     log(f"marker {segment.source_marker}")
+
+        return segments
+    except (
+        FileNotFoundError,
+        PermissionError,
+        IOError,
+        CustomException,
+    ) as error:
+        InfoMsgBox(
+            QMessageBox.Critical,
+            "Error",
+            f"Error: Could not parse sermon segments. Reason: {error}",
+        )
+        sys.exit(1)
+
+
+def upload_sermon_segment(segment: SermonSegment) -> None:
+    try:
+        session = ftplib.FTP_TLS(
+            const.SERMON_UPLOAD_FTP_HOSTNAME,
+            const.SERMON_UPLOAD_FTP_USER,
+            const.SERMON_UPLOAD_FTP_PASSWORD,
+        )
+        session.cwd(const.SERMON_UPLOAD_FTP_UPLOAD_DIR)
+        raw_filenames = session.nlst()
+        disallowed_filenames = []
+        for filename in raw_filenames:
+            if filename != "." and filename != "..":
+                disallowed_filenames.append(filename)
+
+        print(disallowed_filenames)
+        # file = open("upl.mp3", "rb")
+        # session.storbinary("STOR upl.mp3", file)
+        # file.close()
+        session.quit()
+        InfoMsgBox(
+            QMessageBox.Information, "Success", "Sermon uploaded successfully."
+        )
+    except ftplib.all_errors as error:
+        InfoMsgBox(
+            QMessageBox.Critical,
+            "Error",
+            f"Error: Could not connect to ftp server. Reason: {error}",
+        )
+        sys.exit(1)
+
+
+@dataclass
+class ArchiveTypeStrings:
+    archive_type_plural: str
+    action_to_choose: str
+    action_ing_form: str
+
+
+def choose_cd_day() -> list[str]:
+    strings = ArchiveTypeStrings("CD's", "CD day to Burn", "Burning CD for day")
+    return choose_archive_day(strings)
+
+
+def choose_sermon_day() -> list[str]:
+    strings = ArchiveTypeStrings(
+        "Sermons", "Sermon day to upload", "Uploading Sermon for day"
+    )
+    return choose_archive_day(strings)
+
+
+def choose_archive_day(strings: ArchiveTypeStrings) -> list[str]:
+    # pylint: disable=unused-variable
+    app = QApplication([])
+    try:
+        dirs = sorted(listdir(const.CD_RECORD_OUTPUT_BASEDIR))
+        dirs.reverse()
+
+        if not dirs:
+            return [
+                f"Did not find any {strings.archive_type_plural} in: "
+                + f"{const.CD_RECORD_OUTPUT_BASEDIR}.",
+                "",
+            ]
+
+        dialog = RadioButtonDialog(
+            dirs, "Choose a " + f"{strings.action_to_choose}"
+        )
+        if dialog.exec_() == QDialog.Accepted:
+            log(f"{strings.action_ing_form} for day: {dialog.chosen}")
+            return ["", dialog.chosen]
+        return ["ignore", ""]
+    except (FileNotFoundError, PermissionError, IOError):
+        pass
+
+    return [
+        f"Failed to access directory: {const.CD_RECORD_OUTPUT_BASEDIR}.",
+        "",
+    ]
+
+
+def upload_sermon_for_day(yyyy_mm_dd: str):
+    segments = get_possible_sermon_segments_of_day(yyyy_mm_dd)
+    suitable_segments = get_segments_over_20_mins(segments)
+    for segment in suitable_segments:
+        print(f"start {segment.start_frame}")
+        print(f"end {segment.end_frame}")
+        print(f"sheet {segment.source_cue_sheet}")
+        print(f"marker {segment.source_marker}")
+
+    if not segments:
+        # TODO: choose
+        InfoMsgBox(
+            QMessageBox.Critical, "Error", "Error: no suitable segment found"
+        )
+    elif len(segments):
+        upload_sermon_segment(segments[0])
+    else:
+        # TODO: choose
+        pass