set_cd_marker.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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. )
  31. from input import get_cachefile_content, validate_cd_record_config, InfoMsgBox
  32. import config as const
  33. from recording import is_valid_cd_record_checkfile, mark_end_of_recording
  34. def get_reset_marker(yyyy_mm_dd: str) -> int:
  35. max_reset = 0
  36. for file in listdir(path.join(const.CD_RECORD_OUTPUT_BASEDIR, yyyy_mm_dd)):
  37. print(file)
  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. print(f"file {file} reached")
  43. max_reset = max(int(file[11:18]), max_reset)
  44. print(max_reset)
  45. return max_reset + 1
  46. def start_cd_recording() -> None:
  47. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  48. yyyy_mm_dd = get_yyyy_mm_dd_date()
  49. cd_num = get_reset_marker(yyyy_mm_dd)
  50. ensure_output_dir_exists(yyyy_mm_dd)
  51. while cachefile_content[1].strip() != "9001":
  52. filename = path.join(
  53. const.CD_RECORD_OUTPUT_BASEDIR,
  54. yyyy_mm_dd,
  55. f"{yyyy_mm_dd}-{cd_num:0{const.CD_RECORD_FILENAME_ZFILL}}.wav",
  56. )
  57. unix_milis = get_unix_milis()
  58. log(f"starting cd #{cd_num} recording...")
  59. cmd = "ffmpeg -y {} -ar 44100 -t {} {}".format(
  60. const.CD_RECORD_FFMPEG_INPUT_ARGS,
  61. const.CD_RECORD_MAX_SECONDS,
  62. filename,
  63. )
  64. process = Popen(split(cmd))
  65. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  66. log("updating active ffmpeg pid")
  67. try:
  68. with open(cachefile, mode="w+", encoding="utf-8") as file_writer:
  69. file_writer.write(cachefile_content[0].strip() + "\n")
  70. # reset marker to 1
  71. file_writer.write("1\n")
  72. file_writer.write(f"{process.pid}\n")
  73. file_writer.write(f"{unix_milis}\n")
  74. file_writer.write(f"{unix_milis}\n")
  75. file_writer.write(f"{cd_num}\n")
  76. except (FileNotFoundError, PermissionError, IOError) as error:
  77. app = QApplication
  78. InfoMsgBox(
  79. QMessageBox.Critical,
  80. "Error",
  81. "Failed to write to cachefile '{}'. Reason: {}".format(
  82. cachefile, error
  83. ),
  84. )
  85. del app
  86. sys.exit(1)
  87. fresh_cachefile_content = get_cachefile_content(
  88. const.CD_RECORD_CACHEFILE
  89. )
  90. update_cue_sheet(
  91. fresh_cachefile_content, yyyy_mm_dd, unix_milis, initial_run=True
  92. )
  93. _ = process.communicate()[0] # wait for subprocess to end
  94. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  95. cd_num += 1
  96. if process.returncode not in [255, 0]:
  97. mark_end_of_recording(cachefile_content)
  98. app = QApplication
  99. InfoMsgBox(
  100. QMessageBox.Critical,
  101. "Error",
  102. f"ffmpeg terminated with exit code {process.returncode}",
  103. )
  104. del app
  105. sys.exit(1)
  106. def ensure_output_dir_exists(date):
  107. cue_sheet_dir = path.join(expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), date)
  108. try:
  109. if not path.exists(cue_sheet_dir):
  110. mkdir(cue_sheet_dir)
  111. except (FileNotFoundError, PermissionError, IOError) as error:
  112. app = QApplication
  113. InfoMsgBox(
  114. QMessageBox.Critical,
  115. "Error",
  116. "Failed to create to cue sheet directory '{}'. Reason: {}".format(
  117. cue_sheet_dir, error
  118. ),
  119. )
  120. del app
  121. sys.exit(1)
  122. def create_cachefile_for_marker(
  123. cachefile_content: list,
  124. yyyy_mm_dd: str,
  125. unix_milis: int,
  126. initial_run=False,
  127. ) -> None:
  128. cachefile = expand_dir(const.CD_RECORD_CACHEFILE)
  129. if initial_run:
  130. marker = 1
  131. else:
  132. marker = int(cachefile_content[1]) + 1
  133. if marker > 99:
  134. return
  135. if (
  136. not (initial_run)
  137. and unix_milis - int(cachefile_content[4])
  138. < const.CD_RECORD_MIN_TRACK_MILIS
  139. ):
  140. return
  141. log("writing cd marker {} to cachefile...".format(marker))
  142. try:
  143. with open(cachefile, mode="w+", encoding="utf-8") as file_writer:
  144. file_writer.write(f"{yyyy_mm_dd}\n")
  145. file_writer.write(f"{marker}\n")
  146. if initial_run:
  147. file_writer.write("000\n") # fake pid, gets overriden later
  148. file_writer.write(f"{unix_milis}\n")
  149. else:
  150. file_writer.write(f"{cachefile_content[2].strip()}\n")
  151. file_writer.write(f"{cachefile_content[3].strip()}\n")
  152. file_writer.write(f"{unix_milis}\n")
  153. if initial_run:
  154. file_writer.write("1\n")
  155. else:
  156. file_writer.write(f"{cachefile_content[5].strip()}\n")
  157. except (FileNotFoundError, PermissionError, IOError) as error:
  158. app = QApplication
  159. InfoMsgBox(
  160. QMessageBox.Critical,
  161. "Error",
  162. "Failed to write to cachefile '{}'. Reason: {}".format(
  163. cachefile, error
  164. ),
  165. )
  166. del app
  167. sys.exit(1)
  168. def update_cue_sheet(
  169. cachefile_content: list, yyyy_mm_dd: str, unix_milis: int, initial_run=False
  170. ) -> None:
  171. cue_sheet_dir = path.join(
  172. expand_dir(const.CD_RECORD_OUTPUT_BASEDIR), yyyy_mm_dd
  173. )
  174. # use current cachefile data for here cd_num only
  175. fresh_cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  176. cd_num = (
  177. fresh_cachefile_content[5].strip().zfill(const.CD_RECORD_FILENAME_ZFILL)
  178. )
  179. cue_sheet_path = path.join(cue_sheet_dir, f"sheet-{cd_num}.cue")
  180. wave_path = path.join(cue_sheet_dir, f"{yyyy_mm_dd}-{cd_num}.wav")
  181. if initial_run:
  182. log("updating cue sheet...")
  183. try:
  184. if not path.exists(cue_sheet_dir):
  185. mkdir(cue_sheet_dir)
  186. with open(
  187. cue_sheet_path, mode="w+", encoding="utf-8"
  188. ) as file_writer:
  189. file_writer.write(f'FILE "{wave_path}" WAVE\n')
  190. file_writer.write(" TRACK 01 AUDIO\n")
  191. file_writer.write(" INDEX 01 00:00:00\n")
  192. except (FileNotFoundError, PermissionError, IOError) as error:
  193. app = QApplication
  194. InfoMsgBox(
  195. QMessageBox.Critical,
  196. "Error",
  197. "Failed to write to cue sheet file '{}'. Reason: {}".format(
  198. cue_sheet_path, error
  199. ),
  200. )
  201. del app
  202. sys.exit(1)
  203. else:
  204. marker = int(cachefile_content[1]) + 1
  205. if marker > 99:
  206. warn("An Audio CD can only hold up to 99 tracks.")
  207. return
  208. start_milis = int(cachefile_content[3])
  209. last_track_milis = int(cachefile_content[4])
  210. diff_to_max_milis = const.CD_RECORD_MAX_SECONDS * 1000 - (
  211. unix_milis - start_milis
  212. )
  213. if (
  214. not initial_run
  215. and diff_to_max_milis < const.CD_RECORD_MIN_TRACK_MILIS
  216. ):
  217. warn(
  218. "Tried to set CD Marker too close to maximum time, "
  219. + "moving backwards in time..."
  220. )
  221. unix_milis = (
  222. unix_milis - const.CD_RECORD_MIN_TRACK_MILIS + diff_to_max_milis
  223. )
  224. if unix_milis - last_track_milis < const.CD_RECORD_MIN_TRACK_MILIS:
  225. warn(
  226. f"Minimum track length of {const.CD_RECORD_MIN_TRACK_MILIS}"
  227. + "ms not satisfied, skipping..."
  228. )
  229. return
  230. milis_diff = unix_milis - start_milis
  231. mins = milis_diff // 60000
  232. milis_diff -= 60000 * mins
  233. secs = int(milis_diff / 1000)
  234. milis_diff -= 1000 * secs
  235. frames = int(75 / 1000 * milis_diff)
  236. log("updating cue sheet...")
  237. try:
  238. with open(
  239. cue_sheet_path, mode="a", encoding="utf-8"
  240. ) as file_writer:
  241. file_writer.write(" TRACK {:02d} AUDIO\n".format(marker))
  242. file_writer.write(
  243. " INDEX 01 {:02d}:{:02d}:{:02d}\n".format(
  244. mins, secs, frames
  245. )
  246. )
  247. except (FileNotFoundError, PermissionError, IOError) as error:
  248. app = QApplication
  249. InfoMsgBox(
  250. QMessageBox.Critical,
  251. "Error",
  252. "Failed to write to cue sheet file '{}'. Reason: {}".format(
  253. cue_sheet_path, error
  254. ),
  255. )
  256. del app
  257. sys.exit(1)
  258. def set_cd_marker() -> None:
  259. cachefile_content = get_cachefile_content(const.CD_RECORD_CACHEFILE)
  260. yyyy_mm_dd = get_yyyy_mm_dd_date()
  261. unix_milis = get_unix_milis()
  262. cachefile_and_time_data = (cachefile_content, yyyy_mm_dd, unix_milis)
  263. if is_valid_cd_record_checkfile(*cachefile_and_time_data[:-1]):
  264. create_cachefile_for_marker(*cachefile_and_time_data)
  265. update_cue_sheet(*cachefile_and_time_data)
  266. else:
  267. create_cachefile_for_marker(*cachefile_and_time_data, initial_run=True)
  268. update_cue_sheet(*cachefile_and_time_data, initial_run=True)
  269. start_cd_recording()
  270. if __name__ == "__main__":
  271. colorama.init()
  272. validate_cd_record_config()
  273. make_sure_file_exists(const.CD_RECORD_CACHEFILE, gui_error_out=True)
  274. set_cd_marker()