scripts.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  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 get_ffmpeg_timestamp_from_frame(frames: int) -> str:
  256. milis = int(frames / 75 * 1000)
  257. return f"{milis}ms"
  258. def prepare_audio_files_for_segment_chooser(
  259. segments: list[SermonSegment],
  260. ) -> None:
  261. for segment in segments:
  262. # TODO: check if file duration and type roughly match the target to
  263. # avoid useless regenerating. Also, parallelization.
  264. cmd = (
  265. f"ffmpeg -y -i {get_full_wav_path(segment)} -ss "
  266. + f" {get_ffmpeg_timestamp_from_frame(segment.start_frame)} "
  267. + f"-to {get_ffmpeg_timestamp_from_frame(segment.end_frame)} "
  268. + f"-acodec copy {get_audio_base_path_from_segment(segment)}.wav"
  269. )
  270. process = Popen(split(cmd))
  271. _ = process.communicate()[0] # wait for subprocess to end
  272. if process.returncode not in [255, 0]:
  273. app = QApplication([])
  274. InfoMsgBox(
  275. QMessageBox.Critical,
  276. "Error",
  277. "ffmpeg terminated with " + f"exit code {process.returncode}",
  278. )
  279. del app
  280. sys.exit(1)
  281. def exit_as_no_cds_found(target_dir):
  282. InfoMsgBox(
  283. QMessageBox.Critical,
  284. "Error",
  285. f"Error: Did not find any CD's in: {target_dir}.",
  286. )
  287. sys.exit(1)
  288. def is_legal_sheet_filename(filename: str) -> bool:
  289. return bool(match(r"^sheet-[0-9]+\.cue", filename)) and len(filename) == 17
  290. def get_padded_cd_num_from_sheet_filename(filename: str) -> str:
  291. if not is_legal_sheet_filename(filename):
  292. InfoMsgBox(
  293. QMessageBox.Critical,
  294. "Error",
  295. f"Error: filename '{filename}' in illegal format",
  296. )
  297. sys.exit(1)
  298. return filename[6:13]
  299. def burn_and_eject_cd(
  300. yyyy_mm_dd: str, padded_cd_num: str, expect_next_cd=False
  301. ) -> None:
  302. cd_drives = get_cd_drives()
  303. if not cd_drives:
  304. InfoMsgBox(
  305. QMessageBox.Critical,
  306. "Error",
  307. "Error: Could not find a CD-ROM. Please try again",
  308. )
  309. sys.exit(1)
  310. drive = choose_right_cd_drive(cd_drives)
  311. burn_success = CDBurnerGUI(
  312. drive, yyyy_mm_dd, padded_cd_num
  313. ).burning_successful()
  314. if expect_next_cd:
  315. extra_success_msg = "Please put the next CD into the drive slot before clicking the button."
  316. else:
  317. extra_success_msg = ""
  318. if burn_success:
  319. InfoMsgBox(
  320. QMessageBox.Info,
  321. "Info",
  322. "Successfully burned CD." + extra_success_msg,
  323. )
  324. else:
  325. InfoMsgBox(QMessageBox.Critical, "Error", "Error: Failed to burn CD.")
  326. eject_drive(drive)
  327. def make_sure_there_is_no_ongoing_cd_recording() -> None:
  328. if path.isfile(const.CD_RECORD_CACHEFILE):
  329. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  330. if is_valid_cd_record_checkfile(
  331. cachefile_content, get_yyyy_mm_dd_date()
  332. ):
  333. if cachefile_content[1].strip() != "9001":
  334. InfoMsgBox(
  335. QMessageBox.Critical,
  336. "Error",
  337. "Error: Ongoing CD Recording detected",
  338. )
  339. sys.exit(1)
  340. def get_index_line_as_frames(line: str) -> int:
  341. stripped_line = line.strip()
  342. frames = 75 * 60 * int(stripped_line[9:11])
  343. frames += 75 * int(stripped_line[12:14])
  344. frames += int(stripped_line[15:17])
  345. return frames
  346. @dataclass
  347. class SermonSegment:
  348. start_frame: int
  349. end_frame: int
  350. source_cue_sheet: str
  351. source_marker: int
  352. def get_segments_with_suitable_time(
  353. segments: list[SermonSegment],
  354. ) -> list[SermonSegment]:
  355. suitable_segments = []
  356. for segment in segments:
  357. if (
  358. segment.end_frame - segment.start_frame
  359. >= const.SERMON_UPLOAD_SUITABLE_SEGMENT_FRAMES
  360. ):
  361. # if segment.end_frame - segment.start_frame >= 90000: # 75 * 60 * 20
  362. suitable_segments.append(segment)
  363. return suitable_segments
  364. def get_possible_sermon_segments_of_day(yyyy_mm_dd: str) -> list[SermonSegment]:
  365. try:
  366. segments = []
  367. base_frames = 0
  368. max_frames = 0
  369. day_dir = path.join(const.CD_RECORD_OUTPUT_BASEDIR, yyyy_mm_dd)
  370. files = sorted(listdir(day_dir))
  371. cue_sheets = []
  372. for file in files:
  373. if is_legal_sheet_filename(file):
  374. cue_sheets.append(file)
  375. for sheet_num, sheet in enumerate(cue_sheets):
  376. with open(
  377. path.join(day_dir, sheet),
  378. mode="r",
  379. encoding="utf-8-sig",
  380. ) as sheet_reader:
  381. sheet_content = sheet_reader.readlines()
  382. start_frame = 0
  383. end_frame = 0
  384. wav_path = ""
  385. max_line_num = 0
  386. for line_num, line in enumerate(sheet_content):
  387. max_line_num = line_num
  388. if line_num == 0:
  389. if not match(r"^FILE \".+\" WAVE$", line):
  390. raise CustomException("invalid first cue sheet line")
  391. wav_path = line[line.find('"') + 1 :]
  392. wav_path = wav_path[: wav_path.rfind('"')]
  393. elif match(r"^\s+INDEX 01 ([0-9]{2}:){2}[0-9]{2}\s*$", line):
  394. if line_num != 2:
  395. end_frame = get_index_line_as_frames(line)
  396. segments.append(
  397. SermonSegment(
  398. start_frame,
  399. end_frame,
  400. path.join(day_dir, sheet),
  401. (max_line_num - 2) // 2,
  402. )
  403. )
  404. start_frame = end_frame
  405. segments.append(
  406. SermonSegment(
  407. start_frame,
  408. get_wave_duration_in_frames(wav_path),
  409. path.join(day_dir, sheet),
  410. max_line_num // 2,
  411. )
  412. )
  413. # for segment in file_segments:
  414. # log(f"start {segment.start_frame}")
  415. # log(f"end {segment.end_frame}")
  416. # log(f"sheet {segment.source_cue_sheet}")
  417. # log(f"marker {segment.source_marker}")
  418. return segments
  419. except (
  420. FileNotFoundError,
  421. PermissionError,
  422. IOError,
  423. CustomException,
  424. ) as error:
  425. InfoMsgBox(
  426. QMessageBox.Critical,
  427. "Error",
  428. f"Error: Could not parse sermon segments. Reason: {error}",
  429. )
  430. sys.exit(1)
  431. def get_full_wav_path(segment: SermonSegment) -> str:
  432. try:
  433. with open(
  434. segment.source_cue_sheet,
  435. mode="r",
  436. encoding="utf-8-sig",
  437. ) as cue_sheet_reader:
  438. cue_sheet_content = cue_sheet_reader.readlines()
  439. first_line = cue_sheet_content[0].strip()
  440. if not match(r"^FILE \".+\" WAVE$", first_line):
  441. raise CustomException("invalid first cue sheet line")
  442. full_wav_path = first_line[first_line.find('"') + 1 :]
  443. return full_wav_path[: full_wav_path.rfind('"')]
  444. except (
  445. FileNotFoundError,
  446. PermissionError,
  447. IOError,
  448. CustomException,
  449. ) as error:
  450. app = QApplication([])
  451. QMessageBox.critical(
  452. None,
  453. "Error",
  454. f"Could not parse cue sheet: '{segment.source_cue_sheet}',"
  455. + f"Reason: {error}",
  456. )
  457. del app
  458. sys.exit(1)
  459. def make_sermon_segment_mp3(segment: SermonSegment) -> str:
  460. full_wav_path = get_full_wav_path(segment)
  461. mp3_path = f"{get_audio_base_path_from_segment(segment)}.mp3"
  462. cmd = "ffmpeg -y -i {} -acodec libmp3lame {}".format(
  463. full_wav_path,
  464. mp3_path,
  465. )
  466. process = Popen(split(cmd))
  467. _ = process.communicate()[0] # wait for subprocess to end
  468. if process.returncode not in [255, 0]:
  469. app = QApplication([])
  470. InfoMsgBox(
  471. QMessageBox.Critical,
  472. "Error",
  473. "ffmpeg terminated with " + f"exit code {process.returncode}",
  474. )
  475. del app
  476. return mp3_path
  477. def get_audio_base_path_from_segment(segment: SermonSegment) -> str:
  478. splitted_sheet_path = path.split(segment.source_cue_sheet)
  479. mp3_path = path.join(
  480. splitted_sheet_path[0],
  481. splitted_sheet_path[1][6:13] + f"-segment-{segment.source_marker}",
  482. )
  483. return mp3_path
  484. def upload_sermon_segment(segment: SermonSegment) -> None:
  485. try:
  486. session = ftplib.FTP_TLS(
  487. const.SERMON_UPLOAD_FTP_HOSTNAME,
  488. const.SERMON_UPLOAD_FTP_USER,
  489. const.SERMON_UPLOAD_FTP_PASSWORD,
  490. )
  491. session.cwd(const.SERMON_UPLOAD_FTP_UPLOAD_DIR)
  492. raw_filenames = session.nlst()
  493. disallowed_filenames = []
  494. for filename in raw_filenames:
  495. if filename not in (".", ".."):
  496. disallowed_filenames.append(filename)
  497. app = QApplication([])
  498. wanted_filename, accepted_dialog = QInputDialog.getText(
  499. None,
  500. "Input Dialog",
  501. "Enter the filename for the Sermon (the .mp3 can be omitted):",
  502. )
  503. del app
  504. if not wanted_filename.endswith(".mp3"):
  505. wanted_filename = wanted_filename + ".mp3"
  506. if not accepted_dialog or wanted_filename == ".mp3":
  507. session.quit()
  508. sys.exit(0)
  509. if wanted_filename in disallowed_filenames:
  510. InfoMsgBox(
  511. QMessageBox.Critical, "Error", "Error: filename already exists."
  512. )
  513. session.quit()
  514. sys.exit(1)
  515. orig_mp3 = make_sermon_segment_mp3(segment)
  516. mp3_final_path = path.join(path.split(orig_mp3)[0], wanted_filename)
  517. copy2(orig_mp3, mp3_final_path)
  518. with open(mp3_final_path, "rb") as file:
  519. session.storbinary(f"STOR {path.split(mp3_final_path)[1]}", file)
  520. session.quit()
  521. InfoMsgBox(
  522. QMessageBox.Information, "Success", "Sermon uploaded successfully."
  523. )
  524. except (
  525. *ftplib.all_errors,
  526. FileNotFoundError,
  527. PermissionError,
  528. IOError,
  529. ) as error:
  530. InfoMsgBox(
  531. QMessageBox.Critical,
  532. "Error",
  533. f"Error: Could not connect to ftp server. Reason: {error}",
  534. )
  535. sys.exit(1)
  536. @dataclass
  537. class ArchiveTypeStrings:
  538. archive_type_plural: str
  539. action_to_choose: str
  540. action_ing_form: str
  541. def choose_cd_day() -> list[str]:
  542. strings = ArchiveTypeStrings("CD's", "CD day to Burn", "Burning CD for day")
  543. return choose_archive_day(strings)
  544. def choose_sermon_day() -> list[str]:
  545. strings = ArchiveTypeStrings(
  546. "Sermons", "Sermon day to upload", "Uploading Sermon for day"
  547. )
  548. return choose_archive_day(strings)
  549. def choose_archive_day(strings: ArchiveTypeStrings) -> list[str]:
  550. # pylint: disable=unused-variable
  551. app = QApplication([])
  552. try:
  553. dirs = sorted(listdir(const.CD_RECORD_OUTPUT_BASEDIR))
  554. dirs.reverse()
  555. if not dirs:
  556. return [
  557. f"Did not find any {strings.archive_type_plural} in: "
  558. + f"{const.CD_RECORD_OUTPUT_BASEDIR}.",
  559. "",
  560. ]
  561. dialog = RadioButtonDialog(
  562. dirs, "Choose a " + f"{strings.action_to_choose}"
  563. )
  564. if dialog.exec_() == QDialog.Accepted:
  565. log(f"{strings.action_ing_form} for day: {dialog.chosen}")
  566. return ["", dialog.chosen]
  567. return ["ignore", ""]
  568. except (FileNotFoundError, PermissionError, IOError):
  569. pass
  570. return [
  571. f"Failed to access directory: {const.CD_RECORD_OUTPUT_BASEDIR}.",
  572. "",
  573. ]
  574. def upload_sermon_for_day(yyyy_mm_dd: str):
  575. segments = get_possible_sermon_segments_of_day(yyyy_mm_dd)
  576. if not segments:
  577. InfoMsgBox(
  578. QMessageBox.Critical,
  579. "Error",
  580. f"Error: No segment for day '{yyyy_mm_dd}' found",
  581. )
  582. suitable_segments = get_segments_with_suitable_time(segments)
  583. for segment in suitable_segments:
  584. print(f"start {segment.start_frame}")
  585. print(f"end {segment.end_frame}")
  586. print(f"sheet {segment.source_cue_sheet}")
  587. print(f"marker {segment.source_marker}")
  588. if not suitable_segments:
  589. # TODO: choose
  590. InfoMsgBox(
  591. QMessageBox.Critical, "Error", "Error: no suitable segment found"
  592. )
  593. elif len(suitable_segments) == 1:
  594. upload_sermon_segment(suitable_segments[0])
  595. else:
  596. # TODO: choose
  597. pass