cd.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 shlex import split
  15. from subprocess import Popen
  16. from PyQt5.QtWidgets import ( # pylint: disable=no-name-in-module
  17. QDialog,
  18. QApplication,
  19. QMessageBox,
  20. )
  21. from PyQt5.QtCore import ( # pylint: disable=no-name-in-module
  22. QTimer,
  23. )
  24. import config as const
  25. from input import (
  26. InfoMsgBox,
  27. RadioButtonDialog,
  28. validate_cd_burn_config,
  29. WaveAndSheetPreviewChooserGUI,
  30. )
  31. from utils import expand_dir, log, make_sure_file_exists
  32. from os_agnostic import get_cd_drives, eject_drive
  33. from audio import AudioSourceFileType
  34. from .verify import (
  35. is_legal_sheet_filename,
  36. get_padded_cd_num_from_sheet_filename,
  37. )
  38. def get_burn_cmd(cd_drive: str, yyyy_mm_dd, padded_zfill_num: str) -> str:
  39. cue_sheet_path = path.join(
  40. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR),
  41. yyyy_mm_dd,
  42. f"sheet-{padded_zfill_num}.cue",
  43. )
  44. return (
  45. f"cdrecord -pad dev={cd_drive} -dao -swab -text -audio "
  46. + f"-cuefile='{cue_sheet_path}'"
  47. )
  48. class CDBurnerGUI:
  49. def __init__(self, cd_drive: str, yyyy_mm_dd: str, cd_num: str) -> None:
  50. self.app = QApplication([])
  51. self.drive = cd_drive
  52. self.yyyy_mm_dd = yyyy_mm_dd
  53. self.cd_num = cd_num
  54. self.exit_code = 1
  55. self.show_burning_msg_box()
  56. self.start_burn_subprocess()
  57. self.app.exec_()
  58. def burning_successful(self) -> bool:
  59. if self.exit_code == 0:
  60. return True
  61. return False
  62. def show_burning_msg_box(self):
  63. self.message_box = QMessageBox()
  64. self.message_box.setWindowTitle("Info")
  65. self.message_box.setText("Burning CD...")
  66. self.message_box.setInformativeText(
  67. "Please wait for a few minutes. You can close this Window, as "
  68. + "there will spawn another window after the operation is "
  69. + "finished."
  70. )
  71. self.message_box.show()
  72. def start_burn_subprocess(self):
  73. process = Popen(
  74. split(get_burn_cmd(self.drive, self.yyyy_mm_dd, self.cd_num))
  75. )
  76. while process.poll() is None:
  77. QApplication.processEvents()
  78. self.message_box.accept()
  79. # Yeah this is hacky but it doesn't work when calling quit directly
  80. QTimer.singleShot(0, self.app.quit)
  81. self.exit_code = process.returncode
  82. def choose_right_cd_drive(drives: list) -> str:
  83. if len(drives) != 1:
  84. log("Warning: More than one cd drive found", color="yellow")
  85. if (
  86. const.CD_RECORD_PREFERED_DRIVE in drives
  87. and const.CD_RECORD_PREFERED_DRIVE != ""
  88. ):
  89. return const.CD_RECORD_PREFERED_DRIVE
  90. dialog = RadioButtonDialog(drives, "Choose a CD to Burn")
  91. if dialog.exec_() == QDialog.Accepted:
  92. print(f"Dialog accepted: {dialog.chosen_sheets}")
  93. return dialog.chosen_sheets
  94. log("Warning: Choosing first cd drive...", color="yellow")
  95. return drives[0]
  96. def burn_and_eject_cd(
  97. yyyy_mm_dd: str, padded_cd_num: str, expect_next_cd=False
  98. ) -> None:
  99. cd_drives = get_cd_drives()
  100. if not cd_drives:
  101. InfoMsgBox(
  102. QMessageBox.Critical,
  103. "Error",
  104. "Error: Could not find a CD-ROM. Please try again",
  105. )
  106. sys.exit(1)
  107. drive = choose_right_cd_drive(cd_drives)
  108. burn_success = CDBurnerGUI(
  109. drive, yyyy_mm_dd, padded_cd_num
  110. ).burning_successful()
  111. if expect_next_cd:
  112. extra_success_msg = "Please put the next CD into the drive slot before clicking the button."
  113. else:
  114. extra_success_msg = ""
  115. if burn_success:
  116. InfoMsgBox(
  117. QMessageBox.Info,
  118. "Info",
  119. "Successfully burned CD." + extra_success_msg,
  120. )
  121. else:
  122. InfoMsgBox(QMessageBox.Critical, "Error", "Error: Failed to burn CD.")
  123. eject_drive(drive)
  124. def burn_cds_of_day(yyyy_mm_dd: str) -> None:
  125. validate_cd_burn_config()
  126. make_sure_file_exists(const.CD_RECORD_CACHEFILE, gui_error_out=True)
  127. try:
  128. target_dir = path.join(
  129. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), yyyy_mm_dd
  130. )
  131. if not path.isdir(target_dir):
  132. exit_as_no_cds_found(target_dir)
  133. target_files = sorted(listdir(target_dir))
  134. cue_sheets = []
  135. for file in target_files:
  136. if is_legal_sheet_filename(file):
  137. cue_sheets.append(file)
  138. if not target_files:
  139. exit_as_no_cds_found(target_dir)
  140. if len(cue_sheets) == 1:
  141. burn_and_eject_cd(
  142. yyyy_mm_dd, "1".zfill(const.CD_RECORD_FILENAME_ZFILL)
  143. )
  144. else:
  145. app = QApplication([])
  146. dialog = WaveAndSheetPreviewChooserGUI(
  147. target_dir,
  148. cue_sheets,
  149. f"Preview CD's for {yyyy_mm_dd}",
  150. AudioSourceFileType.CUESHEET,
  151. )
  152. if dialog.exec_() == QDialog.Accepted:
  153. if not dialog.chosen_audios:
  154. sys.exit(0)
  155. chosen_sheets = []
  156. for chosen_audio in dialog.chosen_audios:
  157. chosen_sheets.append(chosen_audio.sheet_rel_path)
  158. log(f"Burning CD's from sheets: {chosen_sheets}")
  159. num_of_chosen_sheets = len(dialog.chosen_audios)
  160. for num, sheet in enumerate(chosen_sheets):
  161. del app # pyright: ignore
  162. last_cd_to_burn = num == num_of_chosen_sheets
  163. burn_and_eject_cd(
  164. yyyy_mm_dd,
  165. get_padded_cd_num_from_sheet_filename(sheet),
  166. last_cd_to_burn,
  167. )
  168. except (FileNotFoundError, PermissionError, IOError):
  169. InfoMsgBox(
  170. QMessageBox.Critical,
  171. "Error",
  172. "Error: Could not access directory: "
  173. + f"'{const.CD_RECORD_OUTPUT_BASEDIR}'",
  174. )
  175. sys.exit(1)
  176. def exit_as_no_cds_found(target_dir):
  177. InfoMsgBox(
  178. QMessageBox.Critical,
  179. "Error",
  180. f"Error: Did not find any CD's in: {target_dir}.",
  181. )
  182. sys.exit(1)
  183. def mark_end_of_recording(cachefile_content: list) -> None:
  184. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  185. log("marking end of recording...")
  186. try:
  187. with open(cachefile, mode="w+", encoding="utf-8") as file_writer:
  188. file_writer.write(cachefile_content[0].strip() + "\n")
  189. file_writer.write("9001\n")
  190. file_writer.write(cachefile_content[2].strip() + "\n")
  191. file_writer.write(cachefile_content[3].strip() + "\n")
  192. file_writer.write(cachefile_content[4].strip() + "\n")
  193. file_writer.write(cachefile_content[5].strip() + "\n")
  194. except (FileNotFoundError, PermissionError, IOError) as error:
  195. app = QApplication
  196. InfoMsgBox(
  197. QMessageBox.Critical,
  198. "Error",
  199. "Failed to write to cachefile '{}'. Reason: {}".format(
  200. cachefile, error
  201. ),
  202. )
  203. del app
  204. sys.exit(1)