set_cd_marker.py 9.4 KB

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