downloader.py 14 KB

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