downloader.py 18 KB

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