cd.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. cd_num = padded_cd_num.lstrip("0")
  105. if not cd_drives:
  106. InfoMsgBox(
  107. QMessageBox.Critical,
  108. "Error",
  109. "Error: Could not find a CD-ROM. Please try again",
  110. )
  111. sys.exit(1)
  112. drive = choose_right_cd_drive(cd_drives)
  113. burn_success = CDBurnerGUI(
  114. drive, yyyy_mm_dd, padded_cd_num
  115. ).burning_successful()
  116. if expect_next_cd:
  117. extra_success_msg = "Please put the next CD into the drive slot before clicking the button."
  118. else:
  119. extra_success_msg = ""
  120. eject_drive(drive)
  121. if burn_success:
  122. InfoMsgBox(
  123. QMessageBox.Info,
  124. "Info",
  125. f"Successfully burned CD #{cd_num}. " + extra_success_msg,
  126. )
  127. else:
  128. InfoMsgBox(
  129. QMessageBox.Critical,
  130. "Error",
  131. f"Error: Failed to burn CD #{cd_num}.",
  132. )
  133. def burn_cds_of_day(yyyy_mm_dd: str) -> None:
  134. validate_cd_burn_config()
  135. make_sure_file_exists(const.CD_RECORD_CACHEFILE, gui_error_out=True)
  136. try:
  137. target_dir = path.join(
  138. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), yyyy_mm_dd
  139. )
  140. if not path.isdir(target_dir):
  141. exit_as_no_cds_found(target_dir)
  142. target_files = sorted(listdir(target_dir))
  143. cue_sheets = []
  144. for file in target_files:
  145. if is_legal_sheet_filename(file):
  146. cue_sheets.append(file)
  147. if not target_files:
  148. exit_as_no_cds_found(target_dir)
  149. if len(cue_sheets) == 1:
  150. burn_and_eject_cd(
  151. yyyy_mm_dd, "1".zfill(const.CD_RECORD_FILENAME_ZFILL)
  152. )
  153. else:
  154. app = QApplication([])
  155. dialog = WaveAndSheetPreviewChooserGUI(
  156. target_dir,
  157. cue_sheets,
  158. f"Preview CD's for {yyyy_mm_dd}",
  159. AudioSourceFileType.CUESHEET,
  160. )
  161. if dialog.exec_() == QDialog.Accepted:
  162. if not dialog.chosen_audios:
  163. sys.exit(0)
  164. chosen_sheets = []
  165. for chosen_audio in dialog.chosen_audios:
  166. chosen_sheets.append(chosen_audio.sheet_rel_path)
  167. log(f"Burning CD's from sheets: {chosen_sheets}")
  168. num_of_chosen_sheets = len(dialog.chosen_audios)
  169. for num, sheet in enumerate(chosen_sheets):
  170. if num == 0:
  171. del app # pyright: ignore
  172. last_cd_to_burn = num + 1 == num_of_chosen_sheets
  173. burn_and_eject_cd(
  174. yyyy_mm_dd,
  175. get_padded_cd_num_from_sheet_filename(sheet),
  176. not last_cd_to_burn,
  177. )
  178. except (FileNotFoundError, PermissionError, IOError):
  179. InfoMsgBox(
  180. QMessageBox.Critical,
  181. "Error",
  182. "Error: Could not access directory: "
  183. + f"'{const.CD_RECORD_OUTPUT_BASEDIR}'",
  184. )
  185. sys.exit(1)
  186. def exit_as_no_cds_found(target_dir):
  187. InfoMsgBox(
  188. QMessageBox.Critical,
  189. "Error",
  190. f"Error: Did not find any CD's in: {target_dir}.",
  191. )
  192. sys.exit(1)
  193. def mark_end_of_recording(cachefile_content: list) -> None:
  194. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  195. log("marking end of recording...")
  196. try:
  197. with open(cachefile, mode="w+", encoding="utf-8") as file_writer:
  198. file_writer.write(cachefile_content[0].strip() + "\n")
  199. file_writer.write("9001\n")
  200. file_writer.write(cachefile_content[2].strip() + "\n")
  201. file_writer.write(cachefile_content[3].strip() + "\n")
  202. file_writer.write(cachefile_content[4].strip() + "\n")
  203. file_writer.write(cachefile_content[5].strip() + "\n")
  204. except (FileNotFoundError, PermissionError, IOError) as error:
  205. app = QApplication
  206. InfoMsgBox(
  207. QMessageBox.Critical,
  208. "Error",
  209. "Failed to write to cachefile '{}'. Reason: {}".format(
  210. cachefile, error
  211. ),
  212. )
  213. del app
  214. sys.exit(1)