downloader.py 17 KB

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