set_cd_marker.py 9.8 KB

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