def on_item_finish(_, __, item, file_path): nonlocal downs, existings if file_path is not None: downs += 1 else: existings += 1 bar.set_description_str("[" + "♻︎ Existing".center(15) + "]") time.sleep(0.1) if dump_metadata: bar.set_description_str("[" + "Ⓓ Dumping".center(15) + "]") data_dumper(final_dest, to_datetime(item.created_time), item.as_dict(extra=True)) bar.update(1)
def _down_posts(posts, dest: str = None, directory: str = None, dump_metadata: bool = False): """High-level function for downloading media of a list of posts. Decorates the process with tqdm progress bar. * This function calls `down_containers` function and wraps it with 'for' loop & progress bar to support downloading multiple posts. Arguments: posts: a generator which generates `Post` instances or a list that contains preloaded `Post` instances dest: download destination (should be a directory) directory: make a new directory inside `dest` to store all the files dump_metadata: (force create a sub directory of the post and) dump metadata of each post to a file inside if True Returns: bool: True if file already exists and skipped the download process path: full path to the download destination if download succeeded """ is_preloaded = isinstance(posts, list) path = None total = len(posts) if is_preloaded else None logger.info("Downloading {0} posts {1}...".format( total or "(?)", "with " + str(sum([len(x) for x in posts])) + " media in total" if is_preloaded else "")) # prepare progress bar, hide progress bar when quiet and show download details when debugging with progress(total=total, desc="Processing", ascii=False) as bar: for i, p in enumerate(posts, start=1): bar.set_postfix_str("(" + ( p.shortcode if len(p.shortcode) <= 11 else p.shortcode[:8] + "...") + ") " + p.typename) logger.debug("Downloading {0} of {1} posts...".format( i, total or "(?)")) # download subdir = to_datetime(p.created_time) + "_" + p.shortcode # NOTE: force_subdir if dump_metadata ? path = _down_containers( p, dest, directory, subdir, force_subdir=False ) # `subdir` can also be the filename if the post has only one media # dump metadata if dump_metadata: filename = subdir + ".json" metadata_file = os.path.join( path, filename) # path inside the sub directory logger.debug("-> [{0}] dump metadata".format(filename)) with open(metadata_file, "w+") as f: json.dump(p.as_dict(), f, indent=4) bar.update(1) if path: # path is None if error occurred in `_down_containers()` logger.info("Destination: {0}".format(path)) return path
def cookies_handler(**args): action = args.get("action") insta = load_obj() cookies = [f for f in os.listdir(COOKIES_DIR) if os.path.splitext(f)[1] == ".cookie"] if action == "save": if insta is None: error_print("No session is logged in currently.", exit=1) dump_cookie(insta.my_username, insta.cookies) print(Fore.LIGHTGREEN_EX + Style.BRIGHT + "⬇ Saved cookie for session of " + Fore.WHITE + "@{}".format(insta.my_username)) elif action == "remove": ids = args.get("id") valid = False for id in ids: for i, filename in enumerate(cookies, start=1): if i == id: os.remove(os.path.join(COOKIES_DIR, filename)) print(Fore.LIGHTGREEN_EX + "♻ Removed cookie file of", Style.BRIGHT + "@{}".format(os.path.splitext(filename)[0])) valid = True if not valid: error_print("Invalid ID. No cookie was removed.") else: print("\n" + " " + Style.BRIGHT + "\033[4mSaved Cookies\n" + Style.RESET_ALL) print(Style.BRIGHT + "Location:", COOKIES_DIR, end="\n\n") for i, filename in enumerate(cookies): username, ext = os.path.splitext(filename) # print entry symbol = (Fore.LIGHTYELLOW_EX + "*" + Fore.RESET) if insta and username == insta.my_username else " " modtime = os.path.getmtime(os.path.join(COOKIES_DIR, filename)) expiretime = next(x for x in load_cookie(username, modtime=True) if x.name == "csrftoken").expires print(Fore.MAGENTA + "(" + str(i + 1) + ")", symbol, username) print(Fore.CYAN + "Last Login:"******"%Y-%m-%d %X")) print(Fore.CYAN + "Expire Time:", Fore.LIGHTBLACK_EX + to_datetime(int(expiretime), "%Y-%m-%d %X"), end="\n\n") print(Style.DIM + "If the cookie file of your current logged in session is removed, you will be forced to perform a REAL logout in the next logout action.")
def download(self, dest: str = None, *, write: bool = True, verify: bool = True, on_item_start: Callable = None, on_item_finish: Callable = None, on_item_error: Callable = None): """Download all reel items of this story. Arguments: dest: Path to the destination directory. write: Write file to disk if True, write to memory otherwise. verify: Verify file integrity if True, check the size of file in bytes otherwise. See 'MediaItem.download()'. on_item_start: A callable (Story, int, ReelItem). Called on start of each item. on_item_finish: A callable (Story, int, ReelItem, str). Called on finish of each item. on_item_error: A callable (Story, int, ReelItem, Exception). Called on error of each item. """ dest = os.path.abspath(dest or "./") reel_items = self.reel_items() logger.debug("Downloading {0} ({1} media) [{2}]...".format( repr(self), len(reel_items), self.typename)) logger.debug("Dest: " + dest) for i, item in enumerate(reel_items): if on_item_start is not None: on_item_start(self, i, item) try: filename = to_datetime(item.created_time) file_path = item.download(dest, filename, write=write, verify=verify) if file_path is not None: set_mtime(file_path, item.created_time) if on_item_finish is not None: on_item_finish(self, i, item, file_path) except Exception as e: # NOTE: if the Story has multiple reel items to download, the occurrence of exception will NOT interrupt # the whole download of the story, unless user reraises the exception in 'on_item_error()'. exc_type, exc_value, tb = sys.exc_info() logger.error("{}: {}".format(exc_type.__name__, exc_value)) logger.debug("".join(traceback.format_tb(tb))) if on_item_error is not None: on_item_error(self, i, item, e) continue
def _down_structure(structure, dest: str = None, directory: str = None, subdir: str = None, force_subdir: bool = False): """Download media of containers of a single structure to `dest`. May deecorate the proccess with progress bar. - If there is multiple media in the structure, a sub directory will be created to store the media. * Called in `download_story` and `download_post` individualy. * If a file with the same path and filename found, it will skip the download process. [dest] [directory] [sub directory] (multi or dump_metadata=True) [file] ... Arguments: structure: a structure object that has attrubute `obtain_media()` (`Post` or `Story`) dest: destination path, one will be created if directory not found (must be a directory) directory: make a new directory inside `dest` to store all files subdir: name of the sub directory which is created when downloading multiple media force_subdir: force create a sub directory and store all the media (used when dump_metadata=True) Returns: str: full path to the download destination tuple: (downs, exists) """ dest = dest or "./" path = os.path.abspath(dest) if not os.path.isdir(path): logger.debug("{0} directory not found. Creating one...".format(path)) os.mkdir(path) if directory: path = os.path.join(path, directory) if not os.path.isdir(path): os.mkdir(path) return_path = path media = structure.obtain_media() multi = len(media) > 1 if multi or force_subdir: if subdir: # create a sub directory for multiple media of a post path = os.path.join(path, subdir) if not os.path.isdir(path): os.mkdir(path) logger.debug("Downloading {0} ({1} media) [{2}]...".format(subdir or directory, len(media), structure.typename)) logger.debug("Path: " + path) downs = exists = 0 with progress(len(media), disable=False) as bar: for i, (tname, src) in enumerate(media, start=1): bar.set_postfix_str(tname) if multi: filename = str(i) else: filename = subdir or str(i) if structure.__class__.__name__ in ("Story", "Highlight"): # * exclusively and explictly change filename to datetime string for Story and Highlight filename = to_datetime(structure.created_time_list[i - 1]) # check if the file / directory already exists if os.path.isfile(os.path.join(path, filename + ".jpg")) or os.path.isfile(os.path.join(path, filename + ".mp4")): exists += 1 logger.debug("file already downloaded, skipped !") bar.set_description_str(Back.BLUE + Fore.BLACK + "[" + "Exists".center(11) + "]" + Style.RESET_ALL) time.sleep(0.1) # give some time for displaying the 'Exists' badge of the progress bar else: # download state = _down_from_src(src, filename, path) if state: downs += 1 bar.update(1) return return_path, (downs, exists)
def login_handler(**args): parser = args.get("parser") username = args.get("username") password = args.get("password") cookie_name = None if password and not username: parser.error("username must be specified together with password") insta = load_obj() cookies = [ f for f in os.listdir(COOKIES_DIR) if os.path.splitext(f)[1] == ".cookie" ] if username: for filename in cookies: name, _ = os.path.splitext(filename) if name == username: cookie_name = name break else: if not password: warn_print("No cookie file associated with user", Style.BRIGHT + Fore.WHITE + "'{}'".format(username)) password = prompt("Password: "******"\n" + " " + Style.BRIGHT + "\033[4mSaved Cookies\n" + Style.RESET_ALL) for i, filename in enumerate(cookies): name, ext = os.path.splitext(filename) # print entry symbol = ( Fore.LIGHTYELLOW_EX + "*" + Fore.RESET) if insta and name == insta.my_username else " " mtime = os.path.getmtime(os.path.join(COOKIES_DIR, filename)) print(Fore.MAGENTA + "(" + str(i + 1) + ")", symbol, name) print(Fore.CYAN + "Last Login:"******"%Y-%m-%d %X"), end="\n\n") # print option: Login New Account print(Fore.MAGENTA + "(" + str(len(cookies) + 1) + ")", Fore.LIGHTGREEN_EX + "+", "[Login New Account]\n") # user choice input choice = prompt( "(1-{})choice> ".format(len(cookies) + 1), lambda x: x.isdigit() and 0 < int(x) <= len(cookies) + 1, "invalid index") index = int(choice) - 1 if index == len(cookies): username = prompt("Username: "******"Password: "******"You have already logged into", Style.BRIGHT + Fore.WHITE + "@{}".format(insta.my_username)) yorn = prompt( "Are you sure you want to give up the current session? (Y/n)> ", lambda x: x.isalpha() and (x.lower() in ("y", "n"))) if yorn == "y": logout_handler(real=False) else: error_print("Operation aborted.", exit=1) insta = Instagram() # Login try: insta.login(username, password, load_cookie(cookie_name) if cookie_name else None) # handle 2FA except TwoFactorAuthRequired: code = prompt("[2FA] Security Code: ", lambda x: x.isdigit() and len(str(x)) == 6, "invalid security code") with error_catcher(): insta.two_factor_login(code) # handle Checkpoint except CheckpointChallengeRequired: print( "ⓘ Note that if there is no phone number bound to this account, choosing SMS will fail the challenge." ) mode = prompt("[Checkpoint] (0)-SMS (1)-Email (0|1)> ", lambda x: x.isdigit() and (int(x) in (0, 1)), "invalid choice, only 0 or 1 is accepted") with error_catcher(): auth = insta.checkpoint_challenge_login(int(mode)) code = prompt("[Checkpoint] Security Code: ", lambda x: x.isdigit() and len(str(x)) == 6, "invalid security code") with error_catcher(): auth(code) except InstascrapeError as e: error_print(str(e), exit=1) print("• Dumping cookie...") dump_cookie(insta.my_username, insta.cookies) print("• Dumping object...") dump_obj(insta) if cookie_name: print(Fore.LIGHTBLUE_EX + Style.BRIGHT + "▶︎ Resumed session of " + Fore.WHITE + "@{}".format(insta.my_username)) else: print(Fore.LIGHTGREEN_EX + Style.BRIGHT + "✔ Logged in as " + Fore.WHITE + "@{}".format(insta.my_username))