set_cd_marker.py 9.4 KB

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