set_cd_marker.py 8.4 KB

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