├── .gitignore ├── LICENSE ├── README.md └── pyinstastories.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.json 3 | 4 | __pycache__/ 5 | 6 | stories/ 7 | 8 | *.pyc 9 | 10 | users\.txt 11 | 12 | \.idea/ 13 | 14 | \.vscode/\.ropeproject/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cammy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyInstaStories 2 | ![Version 2.7](https://img.shields.io/badge/Version-2.5-orange.svg) 3 | ![Python 2.7, 3.5](https://img.shields.io/badge/Python-2.7%2C%203.5%2B-3776ab.svg) 4 | 5 | [![Support me!](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/dvingerh) 6 | 7 | Python script to download Instagram stories from a single user or multiple users at once. Supports Python 2.7 and 3.5. 8 | 9 | 10 | # How to use 11 | 12 | Make sure you have the following dependency installed: https://github.com/ping/instagram_private_api 13 | 14 | 15 | ### Arguments 16 | 17 | The `--username` and `--password` arguments are required to generate a new cookie file or when an existing cookie file has expired. You can omit these two arguments if there is a working login cookie file available already. 18 | 19 | `--download` — User(s) to download. Multiple users must be seperated by a space. 20 | 21 | `--batch-file` — Download stories from usernames in a text file. 22 | 23 | `--output` — Destination folder for downloaded stories. If not passed PyInstaLive will take the current working directory as the destination folder. 24 | 25 | `--taken-at` — PyInstaStories will save files with a datetime format: `2019-01-07_22-51-43.jpg` 26 | 27 | `--no-thumbs` — PyInstaStories will skip downloadable video story thumbnail images. 28 | 29 | `--hq-videos` — PyInstaStories will download slightly higher quality video stories. Requires `ffmpeg`. Not stable right now. 30 | 31 | ### Examples 32 | 33 | Download stories of 3 users. 34 | `python3 pyinstastories.py -d jacobsartorius justinbieber lilhankwilliams` 35 | 36 | Download stories of 1 user. Save files with a datetime format and skip downloading of video thumbnail images. 37 | `python3 pyinstastories.py -d iamcardib --taken-at --no-thumbs` 38 | 39 | Download stories from a text file. Pass login username and password as arguments. 40 | `python3 pyinstastories.py --batch-file usernames.txt --username johndoe --password grapefruits` 41 | 42 | ##### Example terminal output 43 | 44 | ``` 45 | $ python3 pyinstastories.py --download justinbieber 46 | ---------------------------------------------------------------------- 47 | [I] PYINSTASTORIES (SCRIPT V2.1 - PYTHON V3.7.3) - 05:55:42 PM 48 | ---------------------------------------------------------------------- 49 | [I] Using cached login cookie for "johndoe". 50 | [I] Login to "johndoe" OK! 51 | [I] Login cookie expiry date: 2019-08-07 at 09:54:43 PM 52 | ---------------------------------------------------------------------- 53 | [I] Files will be downloaded to C:\Users\User\Documents\Git\PyInstaStories 54 | ---------------------------------------------------------------------- 55 | [I] Getting stories for: justinbieber 56 | ---------------------------------------------------------------------- 57 | [I] Downloading video stories. (7 stories detected) 58 | ---------------------------------------------------------------------- 59 | [I] (1/7) Downloading video: 41107421_150110362713394_6909049832863331499_n.mp4 60 | [I] (2/7) Downloading video: 40704767_352431668802214_7535329190798115834_n.mp4 61 | [I] (3/7) Downloading video: 32675407_899984993677896_5838612576283769538_n.mp4 62 | [I] (4/7) Downloading video: 27460743_1232788393557486_4163271676685655927_n.mp4 63 | [I] (5/7) Downloading video: 40991261_591854457989117_3573059593419810351_n.mp4 64 | [I] (6/7) Downloading video: 27449739_373199263333116_2195630862018446526_n.mp4 65 | [I] (7/7) Downloading video: 32786476_689302061513389_6323122299924594750_n.mp4 66 | ---------------------------------------------------------------------- 67 | [I] Downloading image stories. (3 stories detected) 68 | ---------------------------------------------------------------------- 69 | [I] (1/3) Downloading image: 61787819_1607274159404970_4836984492900662152_n.jpg 70 | [I] (2/3) Downloading image: 64505667_498208200986305_7034972402491620659_n.jpg 71 | [I] (3/3) Downloading image: 64264791_1350148401799309_7365462912390446749_n.jpg 72 | ---------------------------------------------------------------------- 73 | [I] Story downloading ended with 3 new images and 7 new videos downloaded. 74 | ---------------------------------------------------------------------- 75 | ``` 76 | -------------------------------------------------------------------------------- /pyinstastories.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import codecs 3 | import datetime 4 | import json 5 | import os 6 | import sys 7 | import time 8 | import subprocess 9 | 10 | from xml.dom.minidom import parseString 11 | 12 | try: 13 | import urllib.request as urllib 14 | except ImportError: 15 | import urllib as urllib 16 | 17 | try: 18 | from instagram_private_api import ( 19 | Client, ClientError, ClientLoginError, 20 | ClientCookieExpiredError, ClientLoginRequiredError, 21 | __version__ as client_version) 22 | except ImportError: 23 | import sys 24 | 25 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 26 | from instagram_private_api import ( 27 | Client, ClientError, ClientLoginError, 28 | ClientCookieExpiredError, ClientLoginRequiredError, 29 | __version__ as client_version) 30 | 31 | from instagram_private_api import ClientError 32 | from instagram_private_api import Client 33 | 34 | script_version = "2.6" 35 | python_version = sys.version.split(' ')[0] 36 | 37 | download_dest = os.getcwd() 38 | 39 | # Login 40 | 41 | 42 | def to_json(python_object): 43 | if isinstance(python_object, bytes): 44 | return {'__class__': 'bytes', 45 | '__value__': codecs.encode(python_object, 'base64').decode()} 46 | raise TypeError(repr(python_object) + ' is not JSON serializable') 47 | 48 | 49 | def from_json(json_object): 50 | if '__class__' in json_object and json_object.get('__class__') == 'bytes': 51 | return codecs.decode(json_object.get('__value__').encode(), 'base64') 52 | return json_object 53 | 54 | 55 | def onlogin_callback(api, settings_file): 56 | cache_settings = api.settings 57 | with open(settings_file, 'w') as outfile: 58 | json.dump(cache_settings, outfile, default=to_json) 59 | print('[I] New auth cookie file was made: {0!s}'.format(settings_file)) 60 | 61 | 62 | def login(username="", password=""): 63 | device_id = None 64 | try: 65 | settings_file = "credentials.json" 66 | if not os.path.isfile(settings_file): 67 | # settings file does not exist 68 | print('[W] Unable to find auth cookie file: {0!s} (creating a new one...)'.format(settings_file)) 69 | 70 | # login new 71 | api = Client( 72 | username, password, 73 | on_login=lambda x: onlogin_callback(x, settings_file)) 74 | else: 75 | with open(settings_file) as file_data: 76 | cached_settings = json.load(file_data, object_hook=from_json) 77 | 78 | device_id = cached_settings.get('device_id') 79 | # reuse auth settings 80 | api = Client( 81 | username, password, 82 | settings=cached_settings) 83 | 84 | print('[I] Using cached login cookie for "' + api.authenticated_user_name + '".') 85 | 86 | except (ClientCookieExpiredError, ClientLoginRequiredError) as e: 87 | print('[E] ClientCookieExpiredError/ClientLoginRequiredError: {0!s}'.format(e)) 88 | 89 | # Login expired 90 | # Do relogin but use default ua, keys and such 91 | if username and password: 92 | api = Client( 93 | username, password, 94 | device_id=device_id, 95 | on_login=lambda x: onlogin_callback(x, settings_file)) 96 | else: 97 | print("[E] The login cookie has expired, but no login arguments were given.") 98 | print("[E] Please supply --username and --password arguments.") 99 | print('-' * 70) 100 | sys.exit(0) 101 | 102 | except ClientLoginError as e: 103 | print('[E] Could not login: {:s}.\n[E] {:s}\n\n{:s}'.format( 104 | json.loads(e.error_response).get("error_title", "Error title not available."), 105 | json.loads(e.error_response).get("message", "Not available"), e.error_response)) 106 | print('-' * 70) 107 | sys.exit(9) 108 | except ClientError as e: 109 | print('[E] Client Error: {:s}'.format(e.error_response)) 110 | print('-' * 70) 111 | sys.exit(9) 112 | except Exception as e: 113 | if str(e).startswith("unsupported pickle protocol"): 114 | print("[W] This cookie file is not compatible with Python {}.".format(sys.version.split(' ')[0][0])) 115 | print("[W] Please delete your cookie file 'credentials.json' and try again.") 116 | else: 117 | print('[E] Unexpected Exception: {0!s}'.format(e)) 118 | print('-' * 70) 119 | sys.exit(99) 120 | 121 | print('[I] Login to "' + api.authenticated_user_name + '" OK!') 122 | cookie_expiry = api.cookie_jar.auth_expires 123 | print('[I] Login cookie expiry date: {0!s}'.format( 124 | datetime.datetime.fromtimestamp(cookie_expiry).strftime('%Y-%m-%d at %I:%M:%S %p'))) 125 | 126 | return api 127 | 128 | 129 | # Downloader 130 | 131 | 132 | def check_directories(user_to_check): 133 | global download_dest 134 | try: 135 | if not os.path.isdir(download_dest + "/stories/{}/".format(user_to_check)): 136 | os.makedirs(download_dest + "/stories/{}/".format(user_to_check)) 137 | return True 138 | except Exception as e: 139 | print(str(e)) 140 | return False 141 | 142 | 143 | def get_media_story(user_to_check, user_id, ig_client, taken_at=False, no_video_thumbs=False, hq_videos=False): 144 | global download_dest 145 | if hq_videos and command_exists("ffmpeg"): 146 | print("[I] Downloading high quality videos enabled. Ffmpeg will be used.") 147 | print('-' * 70) 148 | elif hq_videos and not command_exists("ffmpeg"): 149 | print("[W] Downloading high quality videos enabled but Ffmpeg could not be found. Falling back to default.") 150 | hq_videos = False 151 | print('-' * 70) 152 | try: 153 | try: 154 | feed = ig_client.user_story_feed(user_id) 155 | except Exception as e: 156 | print("[W] An error occurred trying to get user feed: " + str(e)) 157 | return 158 | try: 159 | feed_json = feed['reel']['items'] 160 | open("feed_json.json", 'w').write(json.dumps(feed_json)) 161 | except TypeError as e: 162 | print("[I] There are no recent stories to process for this user.") 163 | return 164 | 165 | list_video_v = [] 166 | list_video_a = [] 167 | list_video = [] 168 | list_image = [] 169 | 170 | list_video_new = [] 171 | list_image_new = [] 172 | 173 | for media in feed_json: 174 | if not taken_at: 175 | taken_ts = None 176 | else: 177 | if media.get('imported_taken_at'): 178 | imported_taken_at = media.get('imported_taken_at', "") 179 | if imported_taken_at > 10000000000: 180 | imported_taken_at /= 1000 181 | taken_ts = datetime.datetime.utcfromtimestamp(media.get('taken_at', "")).strftime( 182 | '%Y-%m-%d_%H-%M-%S') + "__" + datetime.datetime.utcfromtimestamp( 183 | imported_taken_at).strftime( 184 | '%Y-%m-%d_%H-%M-%S') 185 | else: 186 | taken_ts = datetime.datetime.utcfromtimestamp(media.get('taken_at', "")).strftime( 187 | '%Y-%m-%d_%H-%M-%S') 188 | 189 | is_video = 'video_versions' in media and 'image_versions2' in media 190 | 191 | if 'video_versions' in media: 192 | if hq_videos: 193 | video_manifest = parseString(media['video_dash_manifest']) 194 | # video_period = video_manifest.documentElement.getElementsByTagName('Period') 195 | # video_representations = video_period[0].getElementsByTagName('Representation') 196 | # video_url = video_representations.pop().getElementsByTagName('BaseURL')[0].childNodes[0].nodeValue 197 | # audio_url = video_representations[0].getElementsByTagName('BaseURL')[0].childNodes[0].nodeValue 198 | video_period = video_manifest.documentElement.getElementsByTagName('Period') 199 | representations = video_period[0].getElementsByTagName('Representation') 200 | video_url = representations[0].getElementsByTagName('BaseURL')[0].childNodes[0].nodeValue 201 | audio_element = representations.pop() 202 | if audio_element.getAttribute("mimeType") == "audio/mp4": 203 | audio_url = audio_element.getElementsByTagName('BaseURL')[0].childNodes[0].nodeValue 204 | else: 205 | audio_url = "noaudio" 206 | list_video_v.append([video_url, taken_ts]) 207 | list_video_a.append(audio_url) 208 | else: 209 | list_video.append([media['video_versions'][0]['url'], taken_ts]) 210 | if 'image_versions2' in media: 211 | if (is_video and not no_video_thumbs) or not is_video: 212 | list_image.append([media['image_versions2']['candidates'][0]['url'], taken_ts]) 213 | 214 | 215 | if hq_videos: 216 | print("[I] Downloading video stories. ({:d} stories detected)".format(len(list_video_v))) 217 | print('-' * 70) 218 | for index, video in enumerate(list_video_v): 219 | filename = video[0].split('/')[-1] 220 | if taken_at: 221 | try: 222 | final_filename = video[1] + ".mp4" 223 | except: 224 | final_filename = filename.split('.')[0] + ".mp4" 225 | print("[E] Could not determine timestamp filename for this file, using default: ") + final_filename 226 | else: 227 | final_filename = filename.split('.')[0] + ".mp4" 228 | save_path_video = download_dest + "/stories/{}/".format(user_to_check) + final_filename.replace(".mp4", ".video.mp4") 229 | save_path_audio = save_path_video.replace(".video.mp4", ".audio.mp4") 230 | save_path_final = save_path_video.replace(".video.mp4", ".mp4") 231 | if not os.path.exists(save_path_final): 232 | print("[I] ({:d}/{:d}) Downloading video: {:s}".format(index+1, len(list_video_v), final_filename)) 233 | try: 234 | download_file(video[0], save_path_video) 235 | if list_video_a[index] == "noaudio": 236 | has_audio = False 237 | else: 238 | has_audio = True 239 | download_file(list_video_a[index], save_path_audio) 240 | 241 | ffmpeg_binary = os.getenv('FFMPEG_BINARY', 'ffmpeg') 242 | if has_audio: 243 | cmd = [ 244 | ffmpeg_binary, '-loglevel', 'fatal', '-y', 245 | '-i', save_path_video, 246 | '-i', save_path_audio, 247 | '-c:v', 'copy', '-c:a', 'copy', save_path_final] 248 | else: 249 | cmd = [ 250 | ffmpeg_binary, '-loglevel', 'fatal', '-y', 251 | '-i', save_path_video, 252 | '-c:v', 'copy', '-c:a', 'copy', save_path_final] 253 | #fnull = open(os.devnull, 'w') 254 | fnull = None 255 | exit_code = subprocess.call(cmd, stdout=fnull, stderr=subprocess.STDOUT) 256 | if exit_code != 0: 257 | print("[W] FFmpeg exit code not '0' but '{:d}'.".format(exit_code)) 258 | os.remove(save_path_video) 259 | if has_audio: 260 | os.remove(save_path_audio) 261 | return 262 | else: 263 | #print('[I] Ffmpeg generated video: %s' % os.path.basename(save_path_final)) 264 | os.remove(save_path_video) 265 | if has_audio: 266 | os.remove(save_path_audio) 267 | list_video_new.append(save_path_final) 268 | 269 | except Exception as e: 270 | print("[W] An error occurred while iterating HQ video stories: " + str(e)) 271 | exit(1) 272 | else: 273 | print("[I] Story already exists: {:s}".format(final_filename)) 274 | else: 275 | print("[I] Downloading video stories. ({:d} stories detected)".format(len(list_video))) 276 | print('-' * 70) 277 | for index, video in enumerate(list_video): 278 | filename = video[0].split('/')[-1] 279 | if taken_at: 280 | try: 281 | final_filename = video[1] + ".mp4" 282 | except: 283 | final_filename = filename.split('.')[0] + ".mp4" 284 | print("[E] Could not determine timestamp filename for this file, using default: ") + final_filename 285 | else: 286 | final_filename = filename.split('.')[0] + ".mp4" 287 | save_path = download_dest + "/stories/{}/".format(user_to_check) + final_filename 288 | if not os.path.exists(save_path): 289 | print("[I] ({:d}/{:d}) Downloading video: {:s}".format(index+1, len(list_video), final_filename)) 290 | try: 291 | download_file(video[0], save_path) 292 | list_video_new.append(save_path) 293 | except Exception as e: 294 | print("[W] An error occurred while iterating video stories: " + str(e)) 295 | exit(1) 296 | else: 297 | print("[I] Story already exists: {:s}".format(final_filename)) 298 | 299 | print('-' * 70) 300 | print("[I] Downloading image stories. ({:d} stories detected)".format(len(list_image))) 301 | print('-' * 70) 302 | for index, image in enumerate(list_image): 303 | filename = (image[0].split('/')[-1]).split('?', 1)[0] 304 | if taken_at: 305 | try: 306 | final_filename = image[1] + ".jpg" 307 | except: 308 | final_filename = filename.split('.')[0] + ".jpg" 309 | print("[E] Could not determine timestamp filename for this file, using default: ") + final_filename 310 | else: 311 | final_filename = filename.split('.')[0] + ".jpg" 312 | save_path = download_dest + "/stories/{}/".format(user_to_check) + final_filename 313 | if not os.path.exists(save_path): 314 | print("[I] ({:d}/{:d}) Downloading image: {:s}".format(index+1, len(list_image), final_filename)) 315 | try: 316 | download_file(image[0], save_path) 317 | list_image_new.append(save_path) 318 | except Exception as e: 319 | print("[W] An error occurred while iterating image stories: " + str(e)) 320 | exit(1) 321 | else: 322 | print("[I] Story already exists: {:s}".format(final_filename)) 323 | 324 | if (len(list_image_new) != 0) or (len(list_video_new) != 0): 325 | print('-' * 70) 326 | print("[I] Story downloading ended with " + str(len(list_image_new)) + " new images and " + str( 327 | len(list_video_new)) + " new videos downloaded.") 328 | else: 329 | print('-' * 70) 330 | print("[I] No new stories were downloaded.") 331 | except Exception as e: 332 | print("[E] A general error occurred: " + str(e)) 333 | exit(1) 334 | except KeyboardInterrupt as e: 335 | print("[I] User aborted download.") 336 | exit(1) 337 | 338 | def download_file(url, path, attempt=0): 339 | try: 340 | urllib.urlretrieve(url, path) 341 | urllib.urlcleanup() 342 | except Exception as e: 343 | if not attempt == 3: 344 | attempt += 1 345 | print("[E] ({:d}) Download failed: {:s}.".format(attempt, str(e))) 346 | print("[W] Trying again in 5 seconds.") 347 | time.sleep(5) 348 | download_file(url, path, attempt) 349 | else: 350 | print("[E] Retry failed three times, skipping file.") 351 | print('-' * 70) 352 | 353 | def command_exists(command): 354 | try: 355 | fnull = open(os.devnull, 'w') 356 | subprocess.call([command], stdout=fnull, stderr=subprocess.STDOUT) 357 | return True 358 | except OSError: 359 | return False 360 | 361 | def start(): 362 | print("-" * 70) 363 | print('[I] PYINSTASTORIES (SCRIPT V{:s} - PYTHON V{:s}) - {:s}'.format(script_version, python_version, 364 | time.strftime('%I:%M:%S %p'))) 365 | print("-" * 70) 366 | 367 | parser = argparse.ArgumentParser() 368 | parser.add_argument('-u', '--username', dest='username', type=str, required=False, 369 | help="Instagram username to login with.") 370 | parser.add_argument('-p', '--password', dest='password', type=str, required=False, 371 | help="Instagram password to login with.") 372 | parser.add_argument('-d', '--download', nargs='+', dest='download', type=str, required=False, 373 | help="Instagram user to download stories from.") 374 | parser.add_argument('-b,', '--batch-file', dest='batchfile', type=str, required=False, 375 | help="Read a text file of usernames to download stories from.") 376 | parser.add_argument('-ta', '--taken-at', dest='takenat', action='store_true', 377 | help="Append the taken_at timestamp to the filename of downloaded items.") 378 | parser.add_argument('-nt', '--no-thumbs', dest='novideothumbs', action='store_true', 379 | help="Do not download video thumbnails.") 380 | parser.add_argument('-hqv', '--hq-videos', dest='hqvideos', action='store_true', 381 | help="Get higher quality video stories. Requires Ffmpeg.") 382 | parser.add_argument('-o', '--output', dest='output', type=str, required=False, help="Destination folder for downloads.") 383 | 384 | # Workaround to 'disable' argument abbreviations 385 | parser.add_argument('--usernamx', help=argparse.SUPPRESS, metavar='IGNORE') 386 | parser.add_argument('--passworx', help=argparse.SUPPRESS, metavar='IGNORE') 387 | parser.add_argument('--downloax', help=argparse.SUPPRESS, metavar='IGNORE') 388 | parser.add_argument('--batch-filx', help=argparse.SUPPRESS, metavar='IGNORE') 389 | 390 | args, unknown = parser.parse_known_args() 391 | 392 | if args.download or args.batchfile: 393 | if args.download: 394 | users_to_check = args.download 395 | else: 396 | if os.path.isfile(args.batchfile): 397 | users_to_check = [user.rstrip('\n') for user in open(args.batchfile)] 398 | if not users_to_check: 399 | print("[E] The specified file is empty.") 400 | print("-" * 70) 401 | sys.exit(1) 402 | else: 403 | print("[I] downloading {:d} users from batch file.".format(len(users_to_check))) 404 | print("-" * 70) 405 | else: 406 | print('[E] The specified file does not exist.') 407 | print("-" * 70) 408 | sys.exit(1) 409 | else: 410 | print('[E] No usernames provided. Please use the -d or -b argument.') 411 | print("-" * 70) 412 | sys.exit(1) 413 | 414 | if args.username and args.password: 415 | ig_client = login(args.username, args.password) 416 | else: 417 | settings_file = "credentials.json" 418 | if not os.path.isfile(settings_file): 419 | print("[E] No username/password provided, but there is no login cookie present either.") 420 | print("[E] Please supply --username and --password arguments.") 421 | exit(1) 422 | else: 423 | ig_client = login() 424 | 425 | print("-" * 70) 426 | global download_dest 427 | if args.output: 428 | if os.path.isdir(args.output): 429 | download_dest = args.output 430 | else: 431 | print("[W] Destination '{:s}' is invalid, falling back to default location.".format(args.output)) 432 | download_dest = os.getcwd() 433 | print("[I] Files will be downloaded to {:s}".format(download_dest)) 434 | print("-" * 70) 435 | 436 | 437 | def download_user(index, user, attempt=0): 438 | try: 439 | if not user.isdigit(): 440 | user_res = ig_client.username_info(user) 441 | user_id = user_res['user']['pk'] 442 | else: 443 | user_id = user 444 | user_info = ig_client.user_info(user_id) 445 | if not user_info.get("user", None): 446 | raise Exception("No user is associated with the given user id.") 447 | else: 448 | user = user_info.get("user").get("username") 449 | print("[I] Getting stories for: {:s}".format(user)) 450 | print('-' * 70) 451 | if check_directories(user): 452 | follow_res = ig_client.friendships_show(user_id) 453 | if follow_res.get("is_private") and not follow_res.get("following"): 454 | raise Exception("You are not following this private user.") 455 | get_media_story(user, user_id, ig_client, args.takenat, args.novideothumbs, args.hqvideos) 456 | else: 457 | print("[E] Could not make required directories. Please create a 'stories' folder manually.") 458 | exit(1) 459 | if (index + 1) != len(users_to_check): 460 | print('-' * 70) 461 | print('[I] ({}/{}) 5 second time-out until next user...'.format((index + 1), len(users_to_check))) 462 | time.sleep(5) 463 | print('-' * 70) 464 | except Exception as e: 465 | if not attempt == 3: 466 | attempt += 1 467 | print("[E] ({:d}) Download failed: {:s}.".format(attempt, str(e))) 468 | print("[W] Trying again in 5 seconds.") 469 | time.sleep(5) 470 | print('-' * 70) 471 | download_user(index, user, attempt) 472 | else: 473 | print("[E] Retry failed three times, skipping user.") 474 | print('-' * 70) 475 | 476 | for index, user_to_check in enumerate(users_to_check): 477 | try: 478 | download_user(index, user_to_check) 479 | except KeyboardInterrupt: 480 | print('-' * 70) 481 | print("[I] The operation was aborted.") 482 | print('-' * 70) 483 | exit(0) 484 | exit(0) 485 | 486 | 487 | start() 488 | --------------------------------------------------------------------------------