downloader.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import sys
  2. import time
  3. import os
  4. import shutil
  5. import subprocess
  6. import threading
  7. from instagram_private_api_extensions import live, replay
  8. from instagram_private_api import ClientError
  9. from .logger import log, seperator
  10. from .comments import CommentsDownloader
  11. class NoLivestreamException(Exception):
  12. pass
  13. class NoReplayException(Exception):
  14. pass
  15. def main(api_arg, record_arg, settings_arg):
  16. global api
  17. global record
  18. global broadcast
  19. global mpd_url
  20. global settings
  21. settings = settings_arg
  22. api = api_arg
  23. record = record_arg
  24. get_user_info(record)
  25. def run_script(file):
  26. try:
  27. FNULL = open(os.devnull, 'w')
  28. if sys.version.split(' ')[0].startswith('2'):
  29. subprocess.call(["python", file], stdout=FNULL, stderr=subprocess.STDOUT)
  30. else:
  31. subprocess.call(["python3", file], stdout=FNULL, stderr=subprocess.STDOUT)
  32. except OSError as e:
  33. pass
  34. def get_stream_duration(compare_time, broadcast=None):
  35. try:
  36. if broadcast:
  37. record_time = int(time.time()) - int(compare_time)
  38. stream_time = int(time.time()) - int(broadcast['published_time'])
  39. stream_started_mins, stream_started_secs = divmod(stream_time - record_time, 60)
  40. else:
  41. stream_started_mins, stream_started_secs = divmod((int(time.time()) - int(compare_time)), 60)
  42. stream_duration_str = '%d minutes' % stream_started_mins
  43. if stream_started_secs:
  44. stream_duration_str += ' and %d seconds' % stream_started_secs
  45. return stream_duration_str
  46. except:
  47. return "not available"
  48. def record_stream(broadcast):
  49. try:
  50. def print_status(sep=True):
  51. heartbeat_info = api.broadcast_heartbeat_and_viewercount(broadcast['id'])
  52. viewers = broadcast.get('viewer_count', 0)
  53. if sep:
  54. seperator("GREEN")
  55. log('[I] Viewers : ' + str(int(viewers)) + " watching", "GREEN")
  56. log('[I] Airing time : ' + get_stream_duration(broadcast['published_time']), "GREEN")
  57. log('[I] Status : ' + heartbeat_info['broadcast_status'].title(), "GREEN")
  58. return heartbeat_info['broadcast_status'] not in ['active', 'interrupted']
  59. mpd_url = (broadcast.get('dash_manifest')
  60. or broadcast.get('dash_abr_playback_url')
  61. or broadcast['dash_playback_url'])
  62. output_dir = settings.save_path + '{}_{}_{}_{}_live_downloads'.format(settings.current_date, record, broadcast['id'], settings.current_time)
  63. dl = live.Downloader(
  64. mpd=mpd_url,
  65. output_dir=output_dir,
  66. user_agent=api.user_agent,
  67. max_connection_error_retry=3,
  68. duplicate_etag_retry=30,
  69. callback_check=print_status,
  70. mpd_download_timeout=5,
  71. download_timeout=10)
  72. except Exception as e:
  73. log('[E] Could not start downloading livestream: ' + str(e), "RED")
  74. seperator("GREEN")
  75. sys.exit(1)
  76. try:
  77. log('[I] Livestream downloading started...', "GREEN")
  78. seperator("GREEN")
  79. log('[I] Username : ' + record, "GREEN")
  80. print_status(False)
  81. log('[I] MPD URL : ' + mpd_url, "GREEN")
  82. seperator("GREEN")
  83. log('[I] Downloading livestream... press [CTRL+C] to abort.', "GREEN")
  84. if (settings.run_at_start is not "None"):
  85. try:
  86. thread = threading.Thread(target=run_script, args=(settings.run_at_start,))
  87. thread.daemon = True
  88. thread.start()
  89. log("[I] Executed file to run at start.", "GREEN")
  90. except Exception as e:
  91. log('[W] Could not run file: ' + str(e), "YELLOW")
  92. comment_thread_worker = None
  93. if settings.save_comments.title() == "True":
  94. try:
  95. comments_json_file = settings.save_path + '{}_{}_{}_{}_live_comments.json'.format(settings.current_date, record, broadcast['id'], settings.current_time)
  96. comment_thread_worker = threading.Thread(target=get_live_comments, args=(api, broadcast, comments_json_file, dl,))
  97. comment_thread_worker.start()
  98. except Exception as e:
  99. log('[E] An error occurred while checking comments: ' + e, "RED")
  100. dl.run()
  101. seperator("GREEN")
  102. log('[I] The livestream has ended.\n[I] Time recorded : {}\n[I] Stream duration : {}\n[I] Missed time : {}'.format(get_stream_duration(int(settings.current_time)), get_stream_duration(broadcast['published_time']), get_stream_duration(int(settings.current_time), broadcast)), "YELLOW")
  103. seperator("GREEN")
  104. stitch_video(dl, broadcast, comment_thread_worker)
  105. except KeyboardInterrupt:
  106. seperator("GREEN")
  107. log('[I] Download has been aborted by the user.\n[I] Time recorded : {}\n[I] Stream duration : {}\n[I] Missed time : {}'.format(get_stream_duration(int(settings.current_time)), get_stream_duration(broadcast['published_time']), get_stream_duration(int(settings.current_time), broadcast)), "YELLOW")
  108. seperator("GREEN")
  109. if not dl.is_aborted:
  110. dl.stop()
  111. stitch_video(dl, broadcast, comment_thread_worker)
  112. def stitch_video(dl, broadcast, comment_thread_worker):
  113. try:
  114. if comment_thread_worker and comment_thread_worker.is_alive():
  115. log("[I] Stopping comment downloading and saving comments (if any)...", "GREEN")
  116. comment_thread_worker.join()
  117. if (settings.run_at_finish is not "None"):
  118. try:
  119. thread = threading.Thread(target=run_script, args=(settings.run_at_finish,))
  120. thread.daemon = True
  121. thread.start()
  122. log("[I] Executed file to run at finish.", "GREEN")
  123. except Exception as e:
  124. log('[W] Could not run file: ' + e, "YELLOW")
  125. log('[I] Stitching downloaded files into video...', "GREEN")
  126. output_file = settings.save_path + '{}_{}_{}_{}_live.mp4'.format(settings.current_date, record, broadcast['id'], settings.current_time)
  127. try:
  128. if settings.clear_temp_files.title() == "True":
  129. dl.stitch(output_file, cleartempfiles=True)
  130. else:
  131. dl.stitch(output_file, cleartempfiles=False)
  132. log('[I] Successfully stitched downloaded files into video.', "GREEN")
  133. seperator("GREEN")
  134. sys.exit(0)
  135. except Exception as e:
  136. log('[E] Could not stitch downloaded files: ' + str(e), "RED")
  137. seperator("GREEN")
  138. sys.exit(1)
  139. except KeyboardInterrupt:
  140. log('[I] Aborted stitching process, no video was created.', "YELLOW")
  141. seperator("GREEN")
  142. sys.exit(0)
  143. def get_user_info(record):
  144. try:
  145. log('[I] Checking user "' + record + '"...', "GREEN")
  146. user_res = api.username_info(record)
  147. user_id = user_res['user']['pk']
  148. except Exception as e:
  149. log('[E] Could not get user info: ' + str(e), "RED")
  150. seperator("GREEN")
  151. sys.exit(1)
  152. except KeyboardInterrupt:
  153. log('[W] Aborted checking for user.', "YELLOW")
  154. seperator("GREEN")
  155. sys.exit(1)
  156. get_livestreams(user_id)
  157. if settings.save_replays.title() == "True":
  158. get_replays(user_id)
  159. else:
  160. seperator("GREEN")
  161. log("[I] Replay saving is disabled either with a flag or in the config file.", "BLUE")
  162. seperator("GREEN")
  163. sys.exit(0)
  164. def get_livestreams(user_id):
  165. try:
  166. seperator("GREEN")
  167. log('[I] Checking for ongoing livestreams...', "GREEN")
  168. broadcast = api.user_broadcast(user_id)
  169. if (broadcast is None):
  170. raise NoLivestreamException('There are no livestreams available.')
  171. else:
  172. try:
  173. record_stream(broadcast)
  174. except Exception as e:
  175. log('[E] An error occurred while trying to record livestream: ' + str(e), "RED")
  176. seperator("GREEN")
  177. sys.exit(1)
  178. except NoLivestreamException as e:
  179. log('[I] ' + str(e), "YELLOW")
  180. except Exception as e:
  181. if (e.__class__.__name__ is not NoLivestreamException):
  182. log('[E] Could not get livestreams info: ' + str(e), "RED")
  183. seperator("GREEN")
  184. sys.exit(1)
  185. def get_replays(user_id):
  186. try:
  187. seperator("GREEN")
  188. log('[I] Checking for available replays...', "GREEN")
  189. user_story_feed = api.user_story_feed(user_id)
  190. broadcasts = user_story_feed.get('post_live_item', {}).get('broadcasts', [])
  191. except Exception as e:
  192. log('[E] Could not get replay info: ' + str(e), "RED")
  193. seperator("GREEN")
  194. sys.exit(1)
  195. try:
  196. if (len(broadcasts) == 0):
  197. raise NoReplayException('There are no replays available.')
  198. else:
  199. log("[I] Available replays have been found to download, press [CTRL+C] to abort.", "GREEN")
  200. seperator("GREEN")
  201. for index, broadcast in enumerate(broadcasts):
  202. exists = False
  203. if sys.version.split(' ')[0].startswith('2'):
  204. directories = (os.walk(settings.save_path).next()[1])
  205. else:
  206. directories = (os.walk(settings.save_path).__next__()[1])
  207. for directory in directories:
  208. if (str(broadcast['id']) in directory) and ("_live_" not in directory):
  209. log("[W] Already downloaded a replay with ID '" + str(broadcast['id']) + "', skipping...", "GREEN")
  210. exists = True
  211. if not exists:
  212. current = index + 1
  213. log("[I] Downloading replay " + str(current) + " of " + str(len(broadcasts)) + " with ID '" + str(broadcast['id']) + "'...", "GREEN")
  214. current_time = str(int(time.time()))
  215. output_dir = settings.save_path + '{}_{}_{}_{}_replay_downloads'.format(settings.current_date, record, broadcast['id'], settings.current_time)
  216. dl = replay.Downloader(
  217. mpd=broadcast['dash_manifest'],
  218. output_dir=output_dir,
  219. user_agent=api.user_agent)
  220. if settings.clear_temp_files.title() == "True":
  221. replay_saved = dl.download(settings.save_path + '{}_{}_{}_{}_replay.mp4'.format(settings.current_date, record, broadcast['id'], settings.current_time), cleartempfiles=True)
  222. else:
  223. replay_saved = dl.download(settings.save_path + '{}_{}_{}_{}_replay.mp4'.format(settings.current_date, record, broadcast['id'], settings.current_time), cleartempfiles=False)
  224. if settings.save_comments.title() == "True":
  225. log("[I] Checking for available comments to save...", "GREEN")
  226. comments_json_file = settings.save_path + '{}_{}_{}_{}_replay_comments.json'.format(settings.current_date, record, broadcast['id'], settings.current_time)
  227. get_replay_comments(api, broadcast, comments_json_file, dl)
  228. if (len(replay_saved) == 1):
  229. log("[I] Finished downloading replay " + str(current) + " of " + str(len(broadcasts)) + ".", "GREEN")
  230. seperator("GREEN")
  231. else:
  232. log("[W] No output video file was made, please merge the files manually if possible.", "YELLOW")
  233. log("[W] Check if ffmpeg is available by running ffmpeg in your terminal/cmd prompt.", "YELLOW")
  234. log("", "GREEN")
  235. log("[I] Finished downloading all available replays.", "GREEN")
  236. seperator("GREEN")
  237. sys.exit(0)
  238. except NoReplayException as e:
  239. log('[I] ' + str(e), "YELLOW")
  240. seperator("GREEN")
  241. sys.exit(0)
  242. except Exception as e:
  243. log('[E] Could not save replay: ' + str(e), "RED")
  244. seperator("GREEN")
  245. sys.exit(1)
  246. except KeyboardInterrupt:
  247. seperator("GREEN")
  248. log('[I] Download has been aborted by the user.', "YELLOW")
  249. seperator("GREEN")
  250. try:
  251. shutil.rmtree(output_dir)
  252. except Exception as e:
  253. log("[E] Could not remove temp folder: " + str(e), "RED")
  254. sys.exit(1)
  255. sys.exit(0)
  256. def get_replay_comments(api, broadcast, comments_json_file, dl):
  257. cdl = CommentsDownloader(
  258. api=api, broadcast=broadcast, destination_file=comments_json_file)
  259. cdl.get_replay()
  260. try:
  261. if cdl.comments:
  262. comments_log_file = comments_json_file.replace('.json', '.log')
  263. CommentsDownloader.generate_log(
  264. cdl.comments, broadcast['published_time'], comments_log_file,
  265. comments_delay=0)
  266. if len(cdl.comments) == 1:
  267. log("[I] Successfully saved 1 comment to logfile.", "GREEN")
  268. else:
  269. log("[I] Successfully saved {} comments to logfile.".format(len(cdl.comments)), "GREEN")
  270. else:
  271. log("[I] There are no available comments to save.", "GREEN")
  272. except Exception as e:
  273. log('[E] Could not save comments to logfile: ' + str(e), "RED")
  274. def get_live_comments(api, broadcast, comments_json_file, dl):
  275. cdl = CommentsDownloader(
  276. api=api, broadcast=broadcast, destination_file=comments_json_file)
  277. first_comment_created_at = 0
  278. try:
  279. while not dl.is_aborted:
  280. if 'initial_buffered_duration' not in broadcast and dl.initial_buffered_duration:
  281. broadcast['initial_buffered_duration'] = dl.initial_buffered_duration
  282. cdl.broadcast = broadcast
  283. first_comment_created_at = cdl.get_live(first_comment_created_at)
  284. except ClientError as e:
  285. if not 'media has been deleted' in e.error_response:
  286. log("[W] Comment collection ClientError: %d %s" % (e.code, e.error_response), "YELLOW")
  287. try:
  288. if cdl.comments:
  289. cdl.save()
  290. comments_log_file = comments_json_file.replace('.json', '.log')
  291. CommentsDownloader.generate_log(
  292. cdl.comments, settings.current_time, comments_log_file,
  293. comments_delay=dl.initial_buffered_duration)
  294. if len(cdl.comments) == 1:
  295. log("[I] Successfully saved 1 comment to logfile.", "GREEN")
  296. else:
  297. log("[I] Successfully saved {} comments to logfile.".format(len(cdl.comments)), "GREEN")
  298. seperator("GREEN")
  299. else:
  300. log("[I] There are no available comments to save.", "GREEN")
  301. seperator("GREEN")
  302. except Exception as e:
  303. log('[E] Could not save comments to logfile: ' + str(e), "RED")