This is a personal projects that aims to be the perfect online student. It auromatically joins zoom meetings, records classes and mirrors a pre-recorded video to a virtual loopback camera. Keep in mind that this is only meant to work on linux.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
7.0 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. import get_meetings
  2. import subprocess
  3. import datetime
  4. import os
  5. import json
  6. import time
  7. import sys
  8. import discord
  9. from smb.SMBConnection import SMBConnection
  10. RECORD_PATH = "recordings"
  11. LISTENING_VIDEO_PATH = "listening.mkv"
  12. VIRTUAL_CAMERA_PATH = "/dev/video2"
  13. PULSEAUDIO_INDEX = 5
  14. X_OFFSET = sys.argv[1]
  15. PULSEAUDIO_INDEX = sys.argv[2]
  16. AUTO_LOGIN = sys.argv[3] == 1
  17. RECORD_TEMPLATE = ["ffmpeg", "-nostdin", "-video_size", "1920x1080", "-f", "x11grab", "-i", ":0.0+{},0".format(X_OFFSET), "-f", "pulse", "-ac", "2", "-i", str(PULSEAUDIO_INDEX), "-vcodec", "libx264", "-crf", "0", "-preset", "ultrafast", "-acodec", "pcm_s16le"]
  18. CAMERA_TEMPLATE = ["ffmpeg", "-re", "-i", LISTENING_VIDEO_PATH, "-map", "0:v", "-f", "v4l2", VIRTUAL_CAMERA_PATH]
  19. COMPRESSION_TEMPLATE = ["ffmpeg", "-i", None, "-vcodec", "libx265", "-crf", "28", None]
  20. if not os.path.exists(VIRTUAL_CAMERA_PATH):
  21. os.system("sudo modprobe -r v4l2loopback")
  22. os.system("sudo modprobe v4l2loopback")
  23. if not os.path.exists(RECORD_PATH):
  24. os.mkdir(RECORD_PATH)
  25. with open("config.json", "r") as f:
  26. config = json.loads(f.read())
  27. creds = config["creds"]
  28. backup_creds = config["backup"]
  29. DISCORD = config["discord"]
  30. next_day = datetime.datetime.today()
  31. def backup(directory):
  32. conn = SMBConnection(backup_creds["user"], backup_creds["password"],
  33. backup_creds["server_name"], backup_creds["server_name"], use_ntlm_v2 = True)
  34. conn.connect(backup_creds["ip"])
  35. path = "/"
  36. files = conn.listPath(backup_creds["share"], path)
  37. for i in backup_creds["path"].split("/"):
  38. exists = False
  39. path += i + "/"
  40. for f in files:
  41. if f.filename == i:
  42. if f.isDirectory:
  43. exists = True
  44. break
  45. if not exists:
  46. try:
  47. conn.createDirectory(backup_creds["share"],path)
  48. except Exception as e:
  49. print(e)
  50. return 1
  51. files = conn.listPath(backup_creds["share"], path)
  52. folder = conn.getAttributes(backup_creds["share"], backup_creds["path"])
  53. if folder.isReadOnly:
  54. print("BACKUP PATH IS READONLY")
  55. return 1
  56. local_path = os.path.join(RECORD_PATH, directory)
  57. remote_path = os.path.join(backup_creds["path"], directory)
  58. files = conn.listPath(backup_creds["share"], backup_creds["path"])
  59. for f in files:
  60. if f.filename == directory:
  61. print("ALREADY BACKED UP")
  62. return
  63. conn.createDirectory(backup_creds["share"], remote_path)
  64. print("STARTING BACKUP")
  65. for i in os.listdir(local_path):
  66. with open(os.path.join(local_path, i), "rb") as f:
  67. print(i)
  68. conn.storeFile(backup_creds["share"],
  69. os.path.join(remote_path, i),
  70. f)
  71. conn.close()
  72. print("DONE BACKING UP")
  73. if(not AUTO_LOGIN):
  74. client = discord.Client()
  75. @client.event
  76. async def on_ready():
  77. print(f'{client.user} has connected to Discord!')
  78. meetings = get_meetings.get_meetings(creds["TC"], creds["passwd"])
  79. msg = ""
  80. for m in meetings:
  81. msg += "{}: **{}** <{}>\n".format(m["time"],m["class"],m["http_url"])
  82. await client.get_guild(int(DISCORD["guild"])).get_channel(int(DISCORD["channel"])).send(msg)
  83. await client.close()
  84. client.run(DISCORD["token"])
  85. while True:
  86. if not AUTO_LOGIN:
  87. break
  88. now = datetime.datetime.today()
  89. while now < next_day:
  90. time.sleep(7200)
  91. now = datetime.datetime.today()
  92. print("NEW DAY")
  93. print("GENERATING MEETING LIST")
  94. meetings = get_meetings.get_meetings(creds["TC"], creds["passwd"])
  95. print("DONE\n")
  96. day = datetime.datetime.today().strftime("%d_%m_%Y")
  97. next_day = datetime.datetime.strptime("{} 00:00:00".format(day), '%d_%m_%Y %H:%M:%S') + datetime.timedelta(days = 1)
  98. if not os.path.exists(os.path.join(RECORD_PATH, day)):
  99. os.mkdir(os.path.join(RECORD_PATH, day))
  100. video_recording = None
  101. camera_mirroring = None
  102. k = None
  103. for k,i in enumerate(meetings):
  104. now = datetime.datetime.today()
  105. meeting_time = datetime.datetime.strptime("{} {}:00".format(day, i["time"]), '%d_%m_%Y %H:%M:%S')
  106. end_time = datetime.datetime.strptime("{} {}:00".format(day, i["time"]), '%d_%m_%Y %H:%M:%S')+ datetime.timedelta(minutes = 30)
  107. if meeting_time < now:
  108. continue
  109. if camera_mirroring:
  110. while now < end_time:
  111. time.sleep(30)
  112. now = datetime.datetime.today()
  113. print("CLASS FINISHED, TERMINATING PREVIOUS RECORDINGS\n")
  114. if video_recording:
  115. video_recording.terminate()
  116. video_recording = None
  117. camera_mirroring.terminate()
  118. print(i["time"])
  119. print(i["class"])
  120. while now < meeting_time:
  121. time.sleep(30)
  122. now = datetime.datetime.today()
  123. print("STARTING")
  124. kill_proc = subprocess.Popen(["killall","-9","zoom"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  125. kill_proc.wait() # Kill all zoom instances before entering a new meeting
  126. subprocess.Popen(["xdg-open", i["meeting_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  127. cmd = RECORD_TEMPLATE + [os.path.join(RECORD_PATH, day, "{}.{}.mkv".format(k, i["class"]))]
  128. if os.path.exists(os.path.join(RECORD_PATH, day, "{}.{}.mkv".format(k, i["class"]))):
  129. os.remove(os.path.join(RECORD_PATH, day, "{}.{}.mkv".format(k, i["class"])))
  130. blacklisted = False
  131. for j in config["blacklisted"]:
  132. if j in i["class"]:
  133. blacklisted = True
  134. if not blacklisted:
  135. video_recording = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  136. camera_mirroring = subprocess.Popen(CAMERA_TEMPLATE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  137. if k:
  138. end_time = datetime.datetime.strptime("{} {}:00".format(day, meetings[k]["time"]), '%d_%m_%Y %H:%M:%S')+ datetime.timedelta(minutes = 30)
  139. while now < end_time:
  140. time.sleep(30)
  141. now = datetime.datetime.today()
  142. print("CLASS FINISHED, TERMINATING PREVIOUS RECORDINGS\n")
  143. if video_recording:
  144. video_recording.terminate()
  145. if camera_mirroring:
  146. camera_mirroring.terminate()
  147. print("\nALL CLASSES RECORDED\n")
  148. todays_recordings_path = os.path.join(RECORD_PATH, day)
  149. recordings = os.listdir(todays_recordings_path)
  150. for r in recordings:
  151. if r[-3:] != "mkv":
  152. continue
  153. COMPRESSION_TEMPLATE[2] = os.path.join(todays_recordings_path, r)
  154. COMPRESSION_TEMPLATE[-1] = os.path.join(todays_recordings_path, r[:-3] + ".mp4")
  155. print("COMPRESSING " + r)
  156. compression_process = subprocess.Popen(COMPRESSION_TEMPLATE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  157. exit_code = compression_process.wait()
  158. print("FFMPEG EXITED WITH STATUS CODE {}".format(exit_code))
  159. if exit_code == 0:
  160. os.remove(COMPRESSION_TEMPLATE[2])
  161. backup(day)