cd.py 7.8 KB

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