scripts.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. """
  2. Copyright © 2022 Noah Vogt <noah@noahvogt.com>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 3 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. """
  14. import sys
  15. from os import path, listdir
  16. from subprocess import Popen
  17. from shlex import split
  18. from time import sleep
  19. from re import match
  20. from enum import Enum
  21. from dataclasses import dataclass
  22. import ftplib
  23. from shutil import copy2
  24. from pyautogui import keyDown, keyUp
  25. from PyQt5.QtWidgets import ( # pylint: disable=no-name-in-module
  26. QApplication,
  27. QMessageBox,
  28. QInputDialog,
  29. )
  30. from PyQt5.QtWidgets import ( # pylint: disable=no-name-in-module
  31. QDialog,
  32. )
  33. from PyQt5.QtCore import QTimer # pylint: disable=no-name-in-module
  34. from utils import (
  35. log,
  36. error_msg,
  37. get_yyyy_mm_dd_date,
  38. expand_dir,
  39. CustomException,
  40. get_wave_duration_in_frames,
  41. )
  42. from input import (
  43. validate_cd_record_config,
  44. RadioButtonDialog,
  45. InfoMsgBox,
  46. SheetAndPreviewChooser,
  47. get_cachefile_content,
  48. )
  49. from os_agnostic import get_cd_drives, eject_drive
  50. import config as const
  51. def make_sure_file_exists(cachefile: str) -> None:
  52. if not path.isfile(cachefile):
  53. try:
  54. with open(
  55. cachefile, mode="w+", encoding="utf-8-sig"
  56. ) as file_creator:
  57. file_creator.write("")
  58. except (FileNotFoundError, PermissionError, IOError) as error:
  59. error_msg(
  60. "Failed to create file in '{}'. Reason: {}".format(
  61. cachefile, error
  62. )
  63. )
  64. def choose_right_cd_drive(drives: list) -> str:
  65. if len(drives) != 1:
  66. log("Warning: More than one cd drive found", color="yellow")
  67. if (
  68. const.CD_RECORD_PREFERED_DRIVE in drives
  69. and const.CD_RECORD_PREFERED_DRIVE != ""
  70. ):
  71. return const.CD_RECORD_PREFERED_DRIVE
  72. dialog = RadioButtonDialog(drives, "Choose a CD to Burn")
  73. if dialog.exec_() == QDialog.Accepted:
  74. print(f"Dialog accepted: {dialog.chosen_sheets}")
  75. return dialog.chosen_sheets
  76. log("Warning: Choosing first cd drive...", color="yellow")
  77. return drives[0]
  78. def get_burn_cmd(cd_drive: str, yyyy_mm_dd, padded_zfill_num: str) -> str:
  79. cue_sheet_path = path.join(
  80. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR),
  81. yyyy_mm_dd,
  82. f"sheet-{padded_zfill_num}.cue",
  83. )
  84. return (
  85. f"cdrecord -pad dev={cd_drive} -dao -swab -text -audio "
  86. + f"-cuefile='{cue_sheet_path}'"
  87. )
  88. class SongDirection(Enum):
  89. PREVIOUS = "previous"
  90. NEXT = "next"
  91. def cycle_to_song_direction(song_direction: SongDirection):
  92. cachefile_content = get_cachefile_content(const.NEXTSONG_CACHE_FILE)
  93. if song_direction == SongDirection.PREVIOUS:
  94. step = -1
  95. else:
  96. step = 1
  97. if (
  98. not (
  99. len(cachefile_content) == 2
  100. and match(r"[0-9]{4}-[0-9]{2}-[0-9]{2}$", cachefile_content[0])
  101. and match(r"^[0-9]+$", cachefile_content[1])
  102. )
  103. or cachefile_content[0].strip() != get_yyyy_mm_dd_date()
  104. ):
  105. switch_to_song(1)
  106. else:
  107. switch_to_song(int(cachefile_content[1]) + step)
  108. def switch_to_song(song_number: int) -> None:
  109. if song_number > const.OBS_MIN_SUBDIRS:
  110. song_number = 1
  111. if song_number < 1:
  112. song_number = const.OBS_MIN_SUBDIRS
  113. log("sending hotkey to switch to scene {}".format(song_number), "cyan")
  114. scene_switch_hotkey = list(const.OBS_SWITCH_TO_SCENE_HOTKEY_PREFIX)
  115. scene_switch_hotkey.append("f{}".format(song_number))
  116. safe_send_hotkey(scene_switch_hotkey)
  117. log("sending hotkey to transition to scene {}".format(song_number), "cyan")
  118. safe_send_hotkey(const.OBS_TRANSITION_HOTKEY)
  119. create_cachfile_for_song(song_number)
  120. def safe_send_hotkey(hotkey: list, sleep_time=0.1) -> None:
  121. for key in hotkey:
  122. keyDown(key)
  123. sleep(sleep_time)
  124. for key in hotkey:
  125. keyUp(key)
  126. def create_cachfile_for_song(song) -> None:
  127. log("writing song {} to cachefile...".format(song))
  128. try:
  129. with open(
  130. const.NEXTSONG_CACHE_FILE, mode="w", encoding="utf-8-sig"
  131. ) as file_writer:
  132. file_writer.write(get_yyyy_mm_dd_date() + "\n")
  133. file_writer.write(str(song) + "\n")
  134. except (FileNotFoundError, PermissionError, IOError) as error:
  135. error_msg(
  136. "Failed to write to cachefile '{}'. Reason: {}".format(
  137. const.NEXTSONG_CACHE_FILE, error
  138. )
  139. )
  140. def mark_end_of_recording(cachefile_content: list) -> None:
  141. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  142. log("marking end of recording...")
  143. try:
  144. with open(cachefile, mode="w+", encoding="utf-8-sig") as file_writer:
  145. file_writer.write(cachefile_content[0].strip() + "\n")
  146. file_writer.write("9001\n")
  147. file_writer.write(cachefile_content[2].strip() + "\n")
  148. file_writer.write(cachefile_content[3].strip() + "\n")
  149. file_writer.write(cachefile_content[4].strip() + "\n")
  150. file_writer.write(cachefile_content[5].strip() + "\n")
  151. except (FileNotFoundError, PermissionError, IOError) as error:
  152. error_msg(
  153. "Failed to write to cachefile '{}'. Reason: {}".format(
  154. cachefile, error
  155. )
  156. )
  157. def is_valid_cd_record_checkfile(
  158. cachefile_content: list, yyyy_mm_dd: str
  159. ) -> bool:
  160. return (
  161. len(cachefile_content) == 6
  162. # YYYY-MM-DD
  163. and bool(match(r"[0-9]{4}-[0-9]{2}-[0-9]{2}$", cachefile_content[0]))
  164. # last set marker
  165. and bool(match(r"^[0-9][0-9]?$", cachefile_content[1]))
  166. # pid of ffmpeg recording instance
  167. and bool(match(r"^[0-9]+$", cachefile_content[2]))
  168. # unix milis @ recording start
  169. and bool(match(r"^[0-9]+$", cachefile_content[3]))
  170. # unix milis @ last track
  171. and bool(match(r"^[0-9]+$", cachefile_content[4]))
  172. # cd number
  173. and bool(match(r"^[0-9]+$", cachefile_content[5]))
  174. # date matches today
  175. and cachefile_content[0].strip() == yyyy_mm_dd
  176. )
  177. class CDBurnerGUI:
  178. def __init__(self, cd_drive: str, yyyy_mm_dd: str, cd_num: str):
  179. self.app = QApplication([])
  180. self.drive = cd_drive
  181. self.yyyy_mm_dd = yyyy_mm_dd
  182. self.cd_num = cd_num
  183. self.exit_code = 1
  184. self.show_burning_msg_box()
  185. self.start_burn_subprocess()
  186. self.app.exec_()
  187. def burning_successful(self) -> bool:
  188. if self.exit_code == 0:
  189. return True
  190. return False
  191. def show_burning_msg_box(self):
  192. self.message_box = QMessageBox()
  193. self.message_box.setWindowTitle("Info")
  194. self.message_box.setText("Burning CD...")
  195. self.message_box.setInformativeText(
  196. "Please wait for a few minutes. You can close this Window, as "
  197. + "there will spawn another window after the operation is "
  198. + "finished."
  199. )
  200. self.message_box.show()
  201. def start_burn_subprocess(self):
  202. process = Popen(
  203. split(get_burn_cmd(self.drive, self.yyyy_mm_dd, self.cd_num))
  204. )
  205. while process.poll() is None:
  206. QApplication.processEvents()
  207. self.message_box.accept()
  208. # Yeah this is hacky but it doesn't work when calling quit directly
  209. QTimer.singleShot(0, self.app.quit)
  210. self.exit_code = process.returncode
  211. def burn_cds_of_day(yyyy_mm_dd: str) -> None:
  212. validate_cd_record_config()
  213. make_sure_file_exists(const.CD_RECORD_CACHEFILE)
  214. try:
  215. target_dir = path.join(
  216. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), yyyy_mm_dd
  217. )
  218. if not path.isdir(target_dir):
  219. exit_as_no_cds_found(target_dir)
  220. target_files = sorted(listdir(target_dir))
  221. cue_sheets = []
  222. for file in target_files:
  223. if is_legal_sheet_filename(file):
  224. cue_sheets.append(file)
  225. if not target_files:
  226. exit_as_no_cds_found(target_dir)
  227. if len(cue_sheets) == 1:
  228. burn_and_eject_cd(
  229. yyyy_mm_dd, "1".zfill(const.CD_RECORD_FILENAME_ZFILL)
  230. )
  231. else:
  232. app = QApplication([])
  233. dialog = SheetAndPreviewChooser(
  234. target_dir, cue_sheets, f"Preview CD's for {yyyy_mm_dd}"
  235. )
  236. if dialog.exec_() == QDialog.Accepted:
  237. if not dialog.chosen_sheets:
  238. sys.exit(0)
  239. log(f"Burning CD's from sheets: {dialog.chosen_sheets}")
  240. num_of_chosen_sheets = len(dialog.chosen_sheets)
  241. for num, sheet in enumerate(dialog.chosen_sheets):
  242. del app # pyright: ignore
  243. last_cd_to_burn = num == num_of_chosen_sheets
  244. burn_and_eject_cd(
  245. yyyy_mm_dd,
  246. get_padded_cd_num_from_sheet_filename(sheet),
  247. last_cd_to_burn,
  248. )
  249. except (FileNotFoundError, PermissionError, IOError):
  250. InfoMsgBox(
  251. QMessageBox.Critical,
  252. "Error",
  253. "Error: Could not access directory: "
  254. + f"'{const.CD_RECORD_OUTPUT_BASEDIR}'",
  255. )
  256. sys.exit(1)
  257. def exit_as_no_cds_found(target_dir):
  258. InfoMsgBox(
  259. QMessageBox.Critical,
  260. "Error",
  261. f"Error: Did not find any CD's in: {target_dir}.",
  262. )
  263. sys.exit(1)
  264. def is_legal_sheet_filename(filename: str) -> bool:
  265. return bool(match(r"^sheet-[0-9]+\.cue", filename)) and len(filename) == 17
  266. def get_padded_cd_num_from_sheet_filename(filename: str) -> str:
  267. if not is_legal_sheet_filename(filename):
  268. InfoMsgBox(
  269. QMessageBox.Critical,
  270. "Error",
  271. f"Error: filename '{filename}' in illegal format",
  272. )
  273. sys.exit(1)
  274. return filename[6:13]
  275. def burn_and_eject_cd(
  276. yyyy_mm_dd: str, padded_cd_num: str, expect_next_cd=False
  277. ) -> None:
  278. cd_drives = get_cd_drives()
  279. if not cd_drives:
  280. InfoMsgBox(
  281. QMessageBox.Critical,
  282. "Error",
  283. "Error: Could not find a CD-ROM. Please try again",
  284. )
  285. sys.exit(1)
  286. drive = choose_right_cd_drive(cd_drives)
  287. burn_success = CDBurnerGUI(
  288. drive, yyyy_mm_dd, padded_cd_num
  289. ).burning_successful()
  290. if expect_next_cd:
  291. extra_success_msg = "Please put the next CD into the drive slot before clicking the button."
  292. else:
  293. extra_success_msg = ""
  294. if burn_success:
  295. InfoMsgBox(
  296. QMessageBox.Info,
  297. "Info",
  298. "Successfully burned CD." + extra_success_msg,
  299. )
  300. else:
  301. InfoMsgBox(QMessageBox.Critical, "Error", "Error: Failed to burn CD.")
  302. eject_drive(drive)
  303. def make_sure_there_is_no_ongoing_cd_recording() -> None:
  304. if path.isfile(const.CD_RECORD_CACHEFILE):
  305. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  306. if is_valid_cd_record_checkfile(
  307. cachefile_content, get_yyyy_mm_dd_date()
  308. ):
  309. if cachefile_content[1].strip() != "9001":
  310. InfoMsgBox(
  311. QMessageBox.Critical,
  312. "Error",
  313. "Error: Ongoing CD Recording detected",
  314. )
  315. sys.exit(1)
  316. def get_index_line_as_frames(line: str) -> int:
  317. stripped_line = line.strip()
  318. frames = 75 * 60 * int(stripped_line[9:11])
  319. frames += 75 * int(stripped_line[12:14])
  320. frames += int(stripped_line[15:17])
  321. return frames
  322. @dataclass
  323. class SermonSegment:
  324. start_frame: int
  325. end_frame: int
  326. source_cue_sheet: str
  327. source_marker: int
  328. def get_segments_with_suitable_time(
  329. segments: list[SermonSegment],
  330. ) -> list[SermonSegment]:
  331. suitable_segments = []
  332. for segment in segments:
  333. if (
  334. segment.end_frame - segment.start_frame
  335. >= const.SERMON_UPLOAD_SUITABLE_SEGMENT_FRAMES
  336. ):
  337. # if segment.end_frame - segment.start_frame >= 90000: # 75 * 60 * 20
  338. suitable_segments.append(segment)
  339. return suitable_segments
  340. def get_possible_sermon_segments_of_day(yyyy_mm_dd: str) -> list[SermonSegment]:
  341. try:
  342. segments = []
  343. base_frames = 0
  344. max_frames = 0
  345. day_dir = path.join(const.CD_RECORD_OUTPUT_BASEDIR, yyyy_mm_dd)
  346. files = sorted(listdir(day_dir))
  347. cue_sheets = []
  348. for file in files:
  349. if is_legal_sheet_filename(file):
  350. cue_sheets.append(file)
  351. for sheet_num, sheet in enumerate(cue_sheets):
  352. with open(
  353. path.join(day_dir, sheet),
  354. mode="r",
  355. encoding="utf-8-sig",
  356. ) as sheet_reader:
  357. sheet_content = sheet_reader.readlines()
  358. start_frame = 0
  359. end_frame = 0
  360. wav_path = ""
  361. max_line_num = 0
  362. for line_num, line in enumerate(sheet_content):
  363. max_line_num = line_num
  364. if line_num == 0:
  365. if not match(r"^FILE \".+\" WAVE$", line):
  366. raise CustomException("invalid first cue sheet line")
  367. wav_path = line[line.find('"') + 1 :]
  368. wav_path = wav_path[: wav_path.rfind('"')]
  369. elif match(r"^\s+INDEX 01 ([0-9]{2}:){2}[0-9]{2}\s*$", line):
  370. if line_num != 2:
  371. end_frame = get_index_line_as_frames(line)
  372. segments.append(
  373. SermonSegment(
  374. start_frame,
  375. end_frame,
  376. path.join(day_dir, sheet),
  377. (max_line_num - 2) // 2,
  378. )
  379. )
  380. start_frame = end_frame
  381. segments.append(
  382. SermonSegment(
  383. start_frame,
  384. get_wave_duration_in_frames(wav_path),
  385. path.join(day_dir, sheet),
  386. max_line_num // 2,
  387. )
  388. )
  389. # for segment in file_segments:
  390. # log(f"start {segment.start_frame}")
  391. # log(f"end {segment.end_frame}")
  392. # log(f"sheet {segment.source_cue_sheet}")
  393. # log(f"marker {segment.source_marker}")
  394. return segments
  395. except (
  396. FileNotFoundError,
  397. PermissionError,
  398. IOError,
  399. CustomException,
  400. ) as error:
  401. InfoMsgBox(
  402. QMessageBox.Critical,
  403. "Error",
  404. f"Error: Could not parse sermon segments. Reason: {error}",
  405. )
  406. sys.exit(1)
  407. def make_sermon_segment_mp3(segment: SermonSegment) -> str:
  408. try:
  409. with open(
  410. segment.source_cue_sheet,
  411. mode="r",
  412. encoding="utf-8-sig",
  413. ) as cue_sheet_reader:
  414. cue_sheet_content = cue_sheet_reader.readlines()
  415. first_line = cue_sheet_content[0].strip()
  416. if not match(r"^FILE \".+\" WAVE$", first_line):
  417. raise CustomException("invalid first cue sheet line")
  418. full_wav_path = first_line[first_line.find('"') + 1 :]
  419. full_wav_path = full_wav_path[: full_wav_path.rfind('"')]
  420. except (
  421. FileNotFoundError,
  422. PermissionError,
  423. IOError,
  424. CustomException,
  425. ) as error:
  426. app = QApplication([])
  427. QMessageBox.critical(
  428. None,
  429. "Error",
  430. f"Could not parse cue sheet: '{segment.source_cue_sheet}',"
  431. + f"Reason: {error}",
  432. )
  433. del app
  434. sys.exit(1)
  435. splitted_sheet_path = path.split(segment.source_cue_sheet)
  436. mp3_path = path.join(
  437. splitted_sheet_path[0],
  438. splitted_sheet_path[1][6:13] + f"-segment-{segment.source_marker}.mp3",
  439. )
  440. cmd = "ffmpeg -y -i {} -acodec libmp3lame {}".format(
  441. full_wav_path,
  442. mp3_path,
  443. )
  444. process = Popen(split(cmd))
  445. _ = process.communicate()[0] # wait for subprocess to end
  446. if process.returncode not in [255, 0]:
  447. app = QApplication([])
  448. InfoMsgBox(
  449. QMessageBox.Critical,
  450. "Error",
  451. "ffmpeg terminated with " + f"exit code {process.returncode}",
  452. )
  453. return mp3_path
  454. def upload_sermon_segment(segment: SermonSegment) -> None:
  455. try:
  456. session = ftplib.FTP_TLS(
  457. const.SERMON_UPLOAD_FTP_HOSTNAME,
  458. const.SERMON_UPLOAD_FTP_USER,
  459. const.SERMON_UPLOAD_FTP_PASSWORD,
  460. )
  461. session.cwd(const.SERMON_UPLOAD_FTP_UPLOAD_DIR)
  462. raw_filenames = session.nlst()
  463. disallowed_filenames = []
  464. for filename in raw_filenames:
  465. if filename not in (".", ".."):
  466. disallowed_filenames.append(filename)
  467. app = QApplication([])
  468. wanted_filename, accepted_dialog = QInputDialog.getText(
  469. None,
  470. "Input Dialog",
  471. "Enter the filename for the Sermon (the .mp3 can be omitted):",
  472. )
  473. del app
  474. if not wanted_filename.endswith(".mp3"):
  475. wanted_filename = wanted_filename + ".mp3"
  476. if not accepted_dialog or wanted_filename == ".mp3":
  477. session.quit()
  478. sys.exit(0)
  479. if wanted_filename in disallowed_filenames:
  480. InfoMsgBox(
  481. QMessageBox.Critical, "Error", "Error: filename already exists."
  482. )
  483. session.quit()
  484. sys.exit(1)
  485. orig_mp3 = make_sermon_segment_mp3(segment)
  486. mp3_final_path = path.join(path.split(orig_mp3)[0], wanted_filename)
  487. copy2(orig_mp3, mp3_final_path)
  488. with open(mp3_final_path, "rb") as file:
  489. session.storbinary(f"STOR {path.split(mp3_final_path)[1]}", file)
  490. session.quit()
  491. InfoMsgBox(
  492. QMessageBox.Information, "Success", "Sermon uploaded successfully."
  493. )
  494. except (
  495. *ftplib.all_errors,
  496. FileNotFoundError,
  497. PermissionError,
  498. IOError,
  499. ) as error:
  500. InfoMsgBox(
  501. QMessageBox.Critical,
  502. "Error",
  503. f"Error: Could not connect to ftp server. Reason: {error}",
  504. )
  505. sys.exit(1)
  506. @dataclass
  507. class ArchiveTypeStrings:
  508. archive_type_plural: str
  509. action_to_choose: str
  510. action_ing_form: str
  511. def choose_cd_day() -> list[str]:
  512. strings = ArchiveTypeStrings("CD's", "CD day to Burn", "Burning CD for day")
  513. return choose_archive_day(strings)
  514. def choose_sermon_day() -> list[str]:
  515. strings = ArchiveTypeStrings(
  516. "Sermons", "Sermon day to upload", "Uploading Sermon for day"
  517. )
  518. return choose_archive_day(strings)
  519. def choose_archive_day(strings: ArchiveTypeStrings) -> list[str]:
  520. # pylint: disable=unused-variable
  521. app = QApplication([])
  522. try:
  523. dirs = sorted(listdir(const.CD_RECORD_OUTPUT_BASEDIR))
  524. dirs.reverse()
  525. if not dirs:
  526. return [
  527. f"Did not find any {strings.archive_type_plural} in: "
  528. + f"{const.CD_RECORD_OUTPUT_BASEDIR}.",
  529. "",
  530. ]
  531. dialog = RadioButtonDialog(
  532. dirs, "Choose a " + f"{strings.action_to_choose}"
  533. )
  534. if dialog.exec_() == QDialog.Accepted:
  535. log(f"{strings.action_ing_form} for day: {dialog.chosen}")
  536. return ["", dialog.chosen]
  537. return ["ignore", ""]
  538. except (FileNotFoundError, PermissionError, IOError):
  539. pass
  540. return [
  541. f"Failed to access directory: {const.CD_RECORD_OUTPUT_BASEDIR}.",
  542. "",
  543. ]
  544. def upload_sermon_for_day(yyyy_mm_dd: str):
  545. segments = get_possible_sermon_segments_of_day(yyyy_mm_dd)
  546. suitable_segments = get_segments_with_suitable_time(segments)
  547. for segment in suitable_segments:
  548. print(f"start {segment.start_frame}")
  549. print(f"end {segment.end_frame}")
  550. print(f"sheet {segment.source_cue_sheet}")
  551. print(f"marker {segment.source_marker}")
  552. if not suitable_segments:
  553. # TODO: choose
  554. InfoMsgBox(
  555. QMessageBox.Critical, "Error", "Error: no suitable segment found"
  556. )
  557. elif len(suitable_segments) == 1:
  558. upload_sermon_segment(suitable_segments[0])
  559. else:
  560. # TODO: choose
  561. pass