scripts.py 21 KB

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