cd.py 8.0 KB

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