cd.py 7.7 KB

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