set_cd_marker.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. #!/usr/bin/env python3
  2. # Copyright © 2024 Noah Vogt <noah@noahvogt.com>
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. # This program is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU General Public License for more details.
  11. # You should have received a copy of the GNU General Public License
  12. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. import sys
  14. from os import path, mkdir, listdir
  15. from shlex import split
  16. from subprocess import Popen
  17. from re import match
  18. import colorama
  19. from PyQt5.QtWidgets import ( # pylint: disable=no-name-in-module
  20. QApplication,
  21. QMessageBox,
  22. )
  23. from utils import (
  24. get_yyyy_mm_dd_date,
  25. make_sure_file_exists,
  26. get_unix_milis,
  27. log,
  28. warn,
  29. expand_dir,
  30. InfoMsgBox,
  31. )
  32. from input import get_cachefile_content, validate_cd_record_config
  33. import config as const
  34. from recording import is_valid_cd_record_checkfile, mark_end_of_recording
  35. def get_reset_marker(yyyy_mm_dd: str) -> int:
  36. max_reset = 0
  37. for file in listdir(path.join(const.CD_RECORD_OUTPUT_BASEDIR, yyyy_mm_dd)):
  38. if (
  39. match(r"[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]+\.wav$", file)
  40. and len(file) == 22
  41. ):
  42. max_reset = max(int(file[11:18]), max_reset)
  43. return max_reset + 1
  44. def start_cd_recording() -> None:
  45. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  46. yyyy_mm_dd = get_yyyy_mm_dd_date()
  47. cd_num = get_reset_marker(yyyy_mm_dd)
  48. ensure_output_dir_exists(yyyy_mm_dd)
  49. while cachefile_content[1].strip() != "9001":
  50. filename = path.join(
  51. const.CD_RECORD_OUTPUT_BASEDIR,
  52. yyyy_mm_dd,
  53. f"{yyyy_mm_dd}-{cd_num:0{const.CD_RECORD_FILENAME_ZFILL}}.wav",
  54. )
  55. unix_milis = get_unix_milis()
  56. log(f"starting cd #{cd_num} recording...")
  57. cmd = "ffmpeg -y {} -ar 44100 -t {} {}".format(
  58. const.CD_RECORD_FFMPEG_INPUT_ARGS,
  59. const.CD_RECORD_MAX_SECONDS,
  60. filename,
  61. )
  62. process = Popen(split(cmd))
  63. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  64. log("updating active ffmpeg pid")
  65. try:
  66. with open(cachefile, mode="w+", encoding="utf-8") as file_writer:
  67. file_writer.write(cachefile_content[0].strip() + "\n")
  68. # reset marker to 1
  69. file_writer.write("1\n")
  70. file_writer.write(f"{process.pid}\n")
  71. file_writer.write(f"{unix_milis}\n")
  72. file_writer.write(f"{unix_milis}\n")
  73. file_writer.write(f"{cd_num}\n")
  74. except (FileNotFoundError, PermissionError, IOError) as error:
  75. app = QApplication
  76. InfoMsgBox(
  77. QMessageBox.Critical,
  78. "Error",
  79. "Failed to write to cachefile '{}'. Reason: {}".format(
  80. cachefile, error
  81. ),
  82. )
  83. del app
  84. sys.exit(1)
  85. fresh_cachefile_content = get_cachefile_content(
  86. const.CD_RECORD_CACHEFILE
  87. )
  88. update_cue_sheet(
  89. fresh_cachefile_content, yyyy_mm_dd, unix_milis, initial_run=True
  90. )
  91. _ = process.communicate()[0] # wait for subprocess to end
  92. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  93. cd_num += 1
  94. if process.returncode not in [255, 0]:
  95. mark_end_of_recording(cachefile_content)
  96. app = QApplication
  97. InfoMsgBox(
  98. QMessageBox.Critical,
  99. "Error",
  100. f"ffmpeg terminated with exit code {process.returncode}",
  101. )
  102. del app
  103. sys.exit(1)
  104. def ensure_output_dir_exists(date):
  105. cue_sheet_dir = path.join(expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), date)
  106. try:
  107. if not path.exists(cue_sheet_dir):
  108. mkdir(cue_sheet_dir)
  109. except (FileNotFoundError, PermissionError, IOError) as error:
  110. app = QApplication
  111. InfoMsgBox(
  112. QMessageBox.Critical,
  113. "Error",
  114. "Failed to create to cue sheet directory '{}'. Reason: {}".format(
  115. cue_sheet_dir, error
  116. ),
  117. )
  118. del app
  119. sys.exit(1)
  120. def create_cachefile_for_marker(
  121. cachefile_content: list,
  122. yyyy_mm_dd: str,
  123. unix_milis: int,
  124. initial_run=False,
  125. ) -> None:
  126. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  127. if initial_run:
  128. marker = 1
  129. else:
  130. marker = int(cachefile_content[1]) + 1
  131. if marker > 99:
  132. return
  133. if (
  134. not (initial_run)
  135. and unix_milis - int(cachefile_content[4])
  136. < const.CD_RECORD_MIN_TRACK_MILIS
  137. ):
  138. return
  139. log("writing cd marker {} to cachefile...".format(marker))
  140. try:
  141. with open(cachefile, mode="w+", encoding="utf-8") as file_writer:
  142. file_writer.write(f"{yyyy_mm_dd}\n")
  143. file_writer.write(f"{marker}\n")
  144. if initial_run:
  145. file_writer.write("000\n") # fake pid, gets overriden later
  146. file_writer.write(f"{unix_milis}\n")
  147. else:
  148. file_writer.write(f"{cachefile_content[2].strip()}\n")
  149. file_writer.write(f"{cachefile_content[3].strip()}\n")
  150. file_writer.write(f"{unix_milis}\n")
  151. if initial_run:
  152. file_writer.write("1\n")
  153. else:
  154. file_writer.write(f"{cachefile_content[5].strip()}\n")
  155. except (FileNotFoundError, PermissionError, IOError) as error:
  156. app = QApplication
  157. InfoMsgBox(
  158. QMessageBox.Critical,
  159. "Error",
  160. "Failed to write to cachefile '{}'. Reason: {}".format(
  161. cachefile, error
  162. ),
  163. )
  164. del app
  165. sys.exit(1)
  166. def update_cue_sheet(
  167. cachefile_content: list, yyyy_mm_dd: str, unix_milis: int, initial_run=False
  168. ) -> None:
  169. cue_sheet_dir = path.join(
  170. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), yyyy_mm_dd
  171. )
  172. # use current cachefile data for here cd_num only
  173. fresh_cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  174. cd_num = (
  175. fresh_cachefile_content[5].strip().zfill(const.CD_RECORD_FILENAME_ZFILL)
  176. )
  177. cue_sheet_path = path.join(cue_sheet_dir, f"sheet-{cd_num}.cue")
  178. wave_path = path.join(cue_sheet_dir, f"{yyyy_mm_dd}-{cd_num}.wav")
  179. if initial_run:
  180. log("updating cue sheet...")
  181. try:
  182. if not path.exists(cue_sheet_dir):
  183. mkdir(cue_sheet_dir)
  184. with open(
  185. cue_sheet_path, mode="w+", encoding="utf-8"
  186. ) as file_writer:
  187. file_writer.write(f'FILE "{wave_path}" WAVE\n')
  188. file_writer.write(" TRACK 01 AUDIO\n")
  189. file_writer.write(" INDEX 01 00:00:00\n")
  190. except (FileNotFoundError, PermissionError, IOError) as error:
  191. app = QApplication
  192. InfoMsgBox(
  193. QMessageBox.Critical,
  194. "Error",
  195. "Failed to write to cue sheet file '{}'. Reason: {}".format(
  196. cue_sheet_path, error
  197. ),
  198. )
  199. del app
  200. sys.exit(1)
  201. else:
  202. marker = int(cachefile_content[1]) + 1
  203. if marker > 99:
  204. warn("An Audio CD can only hold up to 99 tracks.")
  205. return
  206. start_milis = int(cachefile_content[3])
  207. last_track_milis = int(cachefile_content[4])
  208. diff_to_max_milis = const.CD_RECORD_MAX_SECONDS * 1000 - (
  209. unix_milis - start_milis
  210. )
  211. if (
  212. not initial_run
  213. and diff_to_max_milis < const.CD_RECORD_MIN_TRACK_MILIS
  214. ):
  215. warn(
  216. "Tried to set CD Marker too close to maximum time, "
  217. + "moving backwards in time..."
  218. )
  219. unix_milis = (
  220. unix_milis - const.CD_RECORD_MIN_TRACK_MILIS + diff_to_max_milis
  221. )
  222. if unix_milis - last_track_milis < const.CD_RECORD_MIN_TRACK_MILIS:
  223. warn(
  224. f"Minimum track length of {const.CD_RECORD_MIN_TRACK_MILIS}"
  225. + "ms not satisfied, skipping..."
  226. )
  227. return
  228. milis_diff = unix_milis - start_milis
  229. mins = milis_diff // 60000
  230. milis_diff -= 60000 * mins
  231. secs = int(milis_diff / 1000)
  232. milis_diff -= 1000 * secs
  233. frames = int(75 / 1000 * milis_diff)
  234. log("updating cue sheet...")
  235. try:
  236. with open(
  237. cue_sheet_path, mode="a", encoding="utf-8"
  238. ) as file_writer:
  239. file_writer.write(" TRACK {:02d} AUDIO\n".format(marker))
  240. file_writer.write(
  241. " INDEX 01 {:02d}:{:02d}:{:02d}\n".format(
  242. mins, secs, frames
  243. )
  244. )
  245. except (FileNotFoundError, PermissionError, IOError) as error:
  246. app = QApplication
  247. InfoMsgBox(
  248. QMessageBox.Critical,
  249. "Error",
  250. "Failed to write to cue sheet file '{}'. Reason: {}".format(
  251. cue_sheet_path, error
  252. ),
  253. )
  254. del app
  255. sys.exit(1)
  256. def set_cd_marker() -> None:
  257. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  258. yyyy_mm_dd = get_yyyy_mm_dd_date()
  259. unix_milis = get_unix_milis()
  260. cachefile_and_time_data = (cachefile_content, yyyy_mm_dd, unix_milis)
  261. if is_valid_cd_record_checkfile(*cachefile_and_time_data[:-1]):
  262. create_cachefile_for_marker(*cachefile_and_time_data)
  263. update_cue_sheet(*cachefile_and_time_data)
  264. else:
  265. create_cachefile_for_marker(*cachefile_and_time_data, initial_run=True)
  266. update_cue_sheet(*cachefile_and_time_data, initial_run=True)
  267. start_cd_recording()
  268. if __name__ == "__main__":
  269. colorama.init()
  270. validate_cd_record_config()
  271. make_sure_file_exists(const.CD_RECORD_CACHEFILE, gui_error_out=True)
  272. set_cd_marker()