def logout(args: argparse.Namespace): if args.real: insta = load_obj() if not insta: err_print("You haven't logged in to any account.") return insta.logout() remove_obj() info_print("Logged out", color=Fore.LIGHTBLUE_EX)
def down(args: argparse.Namespace): targets = args.profile count = args.count only = args.only dest = args.dest preload = args.preload dump_metadata = args.dump_metadata before_date = args.before_date after_date = args.after_date if not targets and not args.explore and not args.saved: parser.error("at least one media type must be specified") insta = load_obj() if not insta: err_print("No account logged in") return # ========== Prepare arguments & jobs ========== timestamp_limit = {} try: if before_date: timestamp_limit["before"] = to_timestamp(before_date) if after_date: timestamp_limit["after"] = to_timestamp(after_date) except ValueError: parser.error("incorrect datetime format, should be `YY-mm-dd-h:m:s`") if all((before_date, after_date )) and timestamp_limit["after"] >= timestamp_limit["before"]: parser.error("timestamp limit conflict: `after` >= `before`") down_kwargs = {"dest": dest, "dump_metadata": dump_metadata} post_kwargs = { "count": count or 50, "only": only, "preload": preload, "timestamp_limit": timestamp_limit or None } igtv_kwargs = {"count": count or 50, "preload": preload} profile_jobs = {} # -> -> dict{instance: list[function]} user_jobs = {} # -> dict{instance: list[function]} hashtag_jobs = {} # -> dict{instance: list[function]} post_jobs = {} # -> dict{shortcode: list[function]} other_jobs = [ ] # jobs with no parameters required for calling the functions has_indie_type = False has_highlight_type = False has_igtv_type = False for target in targets: if len(target) < 2: parser.error( "illegal argument parsed in argument: '{0}'".format(target)) if target[0] == "@": # user if target[1] == "#": # tagged if target[2:] not in user_jobs: user_jobs[target[2:]] = [] user_jobs[target[2:]].append( ("Download User Tagged Posts", lambda profile: profile. tagged_posts(**post_kwargs).download(**down_kwargs))) else: # timeline if target[1:] not in user_jobs: user_jobs[target[1:]] = [] user_jobs[target[1:]].append( ("Download User Timeline Posts", lambda profile: profile. timeline_posts(**post_kwargs).download(**down_kwargs))) elif target[0] == "#": # hashtag t = target[1:] if t not in hashtag_jobs: hashtag_jobs[t] = [] hashtag_jobs[t].append( ("Download Hashtag Posts", lambda hashtag: hashtag.posts( **post_kwargs).download(**down_kwargs))) elif target[0] == "%": # story t = target[2:] if target[1] == "@": # user has_indie_type = True if t not in user_jobs: user_jobs[t] = [] user_jobs[t].append( ("Download User Story", lambda profile: profile.story().download(dest=dest))) elif target[1] == "#": # hashtag has_indie_type = True if t not in hashtag_jobs: hashtag_jobs[t] = [] hashtag_jobs[t].append( ("Download Hashtag Story", lambda hashtag: hashtag.story().download(dest=dest))) elif target[1] == "-": # highlights has_highlight_type = True if t not in user_jobs: user_jobs[t] = [] user_jobs[t].append( ("Download User Highlights", lambda profile: profile. highlights(preload=preload).download(dest=dest))) else: parser.error( "illegal symbol parsed in argument: '{0}'".format(target)) elif target[0] == "+": # igtv has_igtv_type = True t = target[1:] if t not in user_jobs: user_jobs[t] = [] user_jobs[t].append( ("Download User IGTV Videos", lambda profile: profile.igtv( **igtv_kwargs).download(**down_kwargs))) elif target[0] == ":": # post has_indie_type = True t = target[1:] post_jobs[t] = [] post_jobs[t].append( ("Download Post", lambda post: post.download(**down_kwargs))) elif target[0] == "/": # profile-pic has_indie_type = True t = target[1:] if t not in user_jobs: user_jobs[t] = [] user_jobs[t].append( ("Download User Profile-Pic", lambda profile: _down_from_src( profile.profile_pic, profile.username, dest))) elif target[0].isalnum(): # profile # specify a new path as to create a seperate directory for storing the whole profile media kwargs = down_kwargs.copy() del kwargs["dest"] profile_jobs[target] = [ ("Download User Timeline Posts", lambda profile: profile.timeline_posts(**post_kwargs). download(dest=os.path.join(dest or "./", profile.username), **kwargs)), ("Download User Tagged Posts", lambda profile: profile.tagged_posts(**post_kwargs).download( dest=os.path.join(dest or "./", profile.username), **kwargs)), ("Download User IGTV Videos", lambda profile: profile.igtv(**igtv_kwargs).download( dest=os.path.join(dest or "./", profile.username), **kwargs)), ("Download User Story", lambda profile: profile.story(). download(dest=os.path.join(dest or "./", profile.username))), ("Download User Highlights", lambda profile: profile.highlights(preload=preload).download( dest=os.path.join(dest or "./", profile.username))), ("Download User Profile-Pic", lambda profile: _down_from_src( profile.profile_pic, profile.username, os.path.join(dest or "./", profile.username))) ] # will override all the previously assigned jobs of this user else: parser.error( "illegal symbol parsed in argument: '{0}'".format(target)) if args.saved: # saved other_jobs.append(("Download My Saved Posts", lambda: insta.me(). saved_posts(**post_kwargs).download(**down_kwargs))) if args.explore: # explore other_jobs.append(("Download Explore Posts", lambda: insta.explore(). posts(**post_kwargs).download(**down_kwargs))) # ========== Download jobs ========== # Handle argument conflicts print(Fore.YELLOW + "Current User:"******"Count:", Style.BRIGHT + str(count or 50)) if has_indie_type and any( (count, only, preload, dump_metadata, before_date, after_date)): err_print( "--count, --only, --preload, --dump-metadata, --before-date, --after-date: not allowed with argument profile_pic (/), post (:), story (%@) (%#)" ) return if has_highlight_type and any( (count, only, before_date, after_date, dump_metadata)): err_print( "--count, --only, --before-date, --after-date, --dump-metadata: not allowed with argument highlights (%-)" ) return if has_igtv_type and any((only, before_date, after_date)): err_print( "--only, --before-date, --after-date: not allowed with argument igtv (+)" ) return def handler(jobs: dict, maker, symbol: str, is_profile_jobs: bool = False): for (target, funcs) in jobs.items(): if is_profile_jobs: print( "\n" + Style.BRIGHT + Fore.LIGHTCYAN_EX + "> \033[4mDownloading User Profile:", Style.BRIGHT + "\033[4m@{0}".format(target)) else: print() instance = maker(target) for (name, func) in funcs: with handle_errors(): info_print("(↓) {0}".format(name), text=symbol + target, color=Fore.LIGHTBLUE_EX) path = func(instance) if path is None: # no download destination path returned because the download failed info_print("(✗) Download Failed", color=Fore.LIGHTRED_EX) else: info_print("(✓) Download Completed =>", text=path, color=Fore.LIGHTGREEN_EX) if is_profile_jobs: print( Style.BRIGHT + Fore.LIGHTCYAN_EX + "> \033[4mCompleted User Profile:", Style.BRIGHT + "\033[4m@{0}".format(target)) if profile_jobs: handler(profile_jobs, lambda target: insta.profile(target), "@", True) if user_jobs: handler(user_jobs, lambda target: insta.profile(target), "@") if hashtag_jobs: handler(hashtag_jobs, lambda target: insta.hashtag(target), "#") if post_jobs: handler(post_jobs, lambda target: insta.post(target), ":") if other_jobs: for (name, func) in other_jobs: print() with handle_errors(): info_print("(↓) {0}".format(name), color=Fore.LIGHTBLUE_EX) path = func() if path is None: # no download destination path returned because the download failed info_print("(✗) Download Failed", color=Fore.LIGHTRED_EX) else: info_print("(✓) Download Completed =>", text=path, color=Fore.LIGHTGREEN_EX)
def dump(args: argparse.Namespace): targets = args.user count = args.count outfile = args.outfile if not targets: parser.error("at least one dump type must be specified") insta = load_obj() if not insta: err_print("No account logged in") return kwargs = {"count": count or 50} ex_kwargs = {"count": count or 50, "convert": False} user_jobs = {} post_jobs = {} for target in targets: if len(target) < 2: parser.error("illegal argument parsed in '{0}'".format(target)) if target[0] == "@": if args.likes or args.comments: parser.error("-likes, -comments: not allowed with @user type") t = target[1:] if t not in user_jobs: user_jobs[t] = [] if not args.followers and not args.followings: if args.count: parser.error("--count: not allowed with argument @user") user_jobs[t].append( ("User Information", lambda profile: profile.as_dict())) else: if args.followers: user_jobs[t].append( ("User Followers", lambda profile: profile.followers(**ex_kwargs))) if args.followings: user_jobs[t].append( ("User Followings", lambda profile: profile.followings(**ex_kwargs))) elif target[0] == ":": if args.followers or args.followings: parser.error( "-followers, -followings: not allowed with :post type") t = target[1:] if t not in user_jobs: post_jobs[t] = [] if not args.likes and not args.comments: if args.count: parser.error("--count: not allowed with argument :post") post_jobs[t].append( ("Post Information", lambda post: post.as_dict())) else: if args.likes: post_jobs[t].append( ("Post Likes", lambda post: post.likes(**ex_kwargs))) if args.comments: post_jobs[t].append(("Post Comments", lambda post: post.comments(**kwargs))) else: parser.error( "illegal symbol parsed in argument: '{0}'".format(target)) print(Fore.YELLOW + "Current User:"******"Count:", Style.BRIGHT + str(count or 50)) def handler(jobs: dict, maker, symbol: str): for target, funcs in jobs.items(): print() instance = maker(target) for (name, func) in funcs: with handle_errors(): # Get data info_print("(Dump) {0}".format(name), text=symbol + target, color=Fore.LIGHTBLUE_EX) data = func(instance) if not data: info_print("(✗) Dump Failed", color=Fore.LIGHTRED_EX) return # Output if outfile: # to file path = os.path.abspath(outfile) if isinstance(data, dict): # => JSON with open(path, "w+") as f: json.dump(data, f, indent=4) else: # => txt with open(path, "w+") as f: buffer = [] for item in data: if isinstance(item, dict): # comments buffer.append(item) else: f.write("@" + str(item) + "\n") if buffer: json.dump(buffer, f, indent=4) info_print("(✓) Dump Succeeded =>", text=path, color=Fore.LIGHTGREEN_EX) else: # to stdout pretty_print(data) if user_jobs: handler(user_jobs, lambda target: insta.profile(target), "@") if post_jobs: handler(post_jobs, lambda target: insta.post(target), ":")
def login(args: argparse.Namespace): username = args.username cookie_file = args.cookie insta = load_obj() my_username = insta.my_username if insta else "" if cookie_file: un = pw = None elif username: # username already provided as command-line arguments filenames = [] for cookie in os.listdir(ACCOUNT_DIR): filename, ext = os.path.splitext(cookie) filenames.append(filename) un = username.strip() if username in filenames: pw = None else: info_print( "Cookie file does not exist yet. Getting credentials...") pw = getpass() if not pw.strip(): parser.error("password should not be empty") else: # List all saved local cookies i = 1 cookies = [] # store raw filenames without the `.cookie` extension files = os.listdir(ACCOUNT_DIR) print(Style.BRIGHT + "\n \033[4m" + "Choose Account\n") for file in files: filename, ext = os.path.splitext(file) if ext != ".cookie": continue cookies.append(filename) # get last modify (login) time try: mtime = time.ctime(os.path.getmtime(ACCOUNT_DIR + file)) except OSError: mtime = time.ctime(os.path.getctime(ACCOUNT_DIR + file)) # print sign = " " if filename == my_username: # current logged in account: use symbol '*' sign = Fore.YELLOW + "*" print(Fore.MAGENTA + "({0})".format(i), sign, filename) print(Fore.CYAN + "Last login:"******"(x)", Fore.YELLOW + "*", my_username) print(Fore.LIGHTBLACK_EX + "Cookie Not Found: deleted from disk\n") # Option for logging in a new account print(Fore.MAGENTA + "({0})".format(len(cookies) + 1), Fore.GREEN + "+", "Login New Account\n") # Prompt try: choice = input("choice> ") except KeyboardInterrupt: return if not choice.isdigit() or not 0 < int(choice) <= len(cookies) + 1: parser.error("invalid choice") choice = int(choice) if choice == len(cookies) + 1: # login to new account un = input("Username: "******"credentials must not be empty") if un in cookies: info_print( "Cookie already exists. Using local saved cookie of the same username." ) else: # use lcoal cookie un = cookies[choice - 1] pw = None # Warn users if they had already logged in to an account if insta and not cookie_file: warn_print("You've already logged in as '{0}'".format(my_username)) ask("Log out from current account and log in to '{0}'".format(un)) # Load cookie file if supplied cookie_data = None if cookie_file: cookie_file = os.path.expanduser(os.path.abspath(cookie_file)) try: with open(cookie_file, "r") as f: cookie_data = json.load(f) except FileNotFoundError: err_print("Cookie file '{0}' does not exist".format(cookie_file)) return except json.JSONDecodeError: err_print("Cookie file is not a valid JSON format") return # Initialize and login with `InstasSraper` object insta = InstaScraper( username=un, password=pw, cookie=cookie_data ) # ! no need to provide logger `level`, as the logger was set up in `main()`. try: insta.login() # keep the logged in state and store it in the pickle except TwoFactorAuthRequired: info_print("Two-Factor Authentication is required.") info_print( "The security code has been sent to your phone through SMS.") code = input("Security Code: ") if not code.strip(): parser.error("security code must not be empty") insta.two_factor_login(code) dump_obj(insta) info_print("Logged in as '{0}'".format(insta.my_username), color=Fore.LIGHTBLUE_EX)
def down(args: argparse.Namespace): targets = args.profile count = args.count only = args.only dest = args.dest preload = args.preload dump_metadata = args.dump_metadata before_date = args.before_date after_date = args.after_date if not targets and not args.explore and not args.saved: parser.error("at least one media type must be specified") insta = load_obj() if not insta: err_print("No account logged in") return # ========== Prepare arguments & jobs ========== timestamp_limit = {} try: if before_date: timestamp_limit["before"] = to_timestamp(before_date) if after_date: timestamp_limit["after"] = to_timestamp(after_date) except ValueError: parser.error("incorrect datetime format, should be `YY-mm-dd-h:m:s`") if all((before_date, after_date )) and timestamp_limit["after"] >= timestamp_limit["before"]: parser.error( "timestamp limit conflict: `after` is greater than or equal to `before`" ) kwargs = { "count": count or 50, "only": only, "dest": dest, "preload": preload, "dump_metadata": dump_metadata, "timestamp_limit": timestamp_limit or None } ex_kwargs = {"dest": dest} # -> kwargs for individuals has_individual = False # -> has one of 'story', 'post', 'profile-pic' profile_jobs = [ ] # -> profile job queue -> tuple(target text, list[tuple(function, (args), kwargs, target text),...]) jobs = [] # job queue -> tuple(function, (args), kwargs, target text) for target in targets: if len(target) < 2: parser.error("illegal argument parsed in '{0}'".format(target)) if target[0] == "@": # user if target[1] == "#": # tagged jobs.append((insta.download_user_tagged_posts, (target[2:], ), kwargs, target)) else: # timeline jobs.append((insta.download_user_timeline_posts, (target[1:], ), kwargs, target)) elif target[0] == "#": # hashtag jobs.append( (insta.download_hashtag_posts, (target[1:], ), kwargs, target)) elif target[0] == "%": # story if target[1] == "@": # user story_args = (target[2:], None) elif target[1] == "#": # hashtag story_args = (None, target[2:]) else: parser.error( "illegal symbol parsed in argument: '{0}'".format(target)) has_individual = True jobs.append((insta.download_story, story_args, ex_kwargs, target)) elif target[0] == ":": # post has_individual = True jobs.append( (insta.download_post, (target[1:], ), ex_kwargs, target)) elif target[0] == "/": # profile-pic has_individual = True jobs.append((insta.download_user_profile_pic, (target[1:], ), ex_kwargs, target)) elif target[0].isalpha() or target[0].isdigit(): # profile # specify a new path as to create a seperate directory for storing the whole profile media profile_path = os.path.join(dest or "./", target) profile_kwargs = kwargs.copy() profile_ex_kwargs = ex_kwargs.copy() profile_kwargs.update({"dest": profile_path}) profile_ex_kwargs.update({"dest": profile_path}) temp = [(insta.download_user_timeline_posts, (target, ), profile_kwargs), (insta.download_user_tagged_posts, (target, ), profile_kwargs), (insta.download_story, (target, None), profile_ex_kwargs), (insta.download_user_profile_pic, (target, ), profile_ex_kwargs)] profile_jobs.append((target, temp)) else: parser.error( "illegal symbol parsed in argument: '{0}'".format(target)) if args.saved: # saved jobs.append((insta.download_self_saved_posts, (), kwargs, None)) if args.explore: # explore jobs.append((insta.download_explore_posts, (), kwargs, None)) # Handle profiles download if profile_jobs and jobs: # arguments conflict -> cannot download both profiles and other types at the same time parser.error( "cannot specify download both 'profile' type and other types of media at the same time" ) # ========== Download jobs ========== print(Fore.YELLOW + "Current User:"******"Count:", Style.BRIGHT + str(count or 50)) if has_individual and any((count, only, preload, dump_metadata)): warn_print( "--count, --only, --preload --dump-metadata: not allowed with argument profile_pic (/), post (:), story (%@) (%#)" ) # Handle profile jobs if profile_jobs: for target, profiles in profile_jobs: print( "\n" + Style.BRIGHT + Fore.LIGHTCYAN_EX + "> \033[4mDownloading User Profile:", Style.BRIGHT + "\033[4m@{0}".format(target)) for i, (function, arguments, kwargs) in enumerate(profiles, start=1): with handle_errors(current_function_name=function.__name__, is_final=i == len(jobs)): info_print("(↓) {0}".format( function.__name__.title().replace("_", " ")), text=target if target else None, color=Fore.LIGHTBLUE_EX) # deepcopy `InstaScraper` object to ensure better safety path = function(*arguments, **kwargs) if path is None: # no download destination path returned because the download failed info_print("(✗) Download Failed", color=Fore.LIGHTRED_EX) else: info_print("(✓) Download Completed =>", text=path, color=Fore.LIGHTGREEN_EX) print( Style.BRIGHT + Fore.LIGHTCYAN_EX + "> \033[4mCompleted User Profile:", Style.BRIGHT + "\033[4m@{0}".format(target)) # Handle seperate jobs if jobs: # retrieve functions and arguments from the job queue for i, (function, arguments, kwargs, target) in enumerate(jobs, start=1): print() with handle_errors(current_function_name=function.__name__, is_final=i == len(jobs)): info_print("(↓) {0}".format(function.__name__.title().replace( "_", " ")), text=target if target else None, color=Fore.LIGHTBLUE_EX) # deepcopy `InstaScraper` object to ensure better safety path = function(*arguments, **kwargs) if path is None: # no download destination path returned because of download failed info_print("(✗) Download Failed", color=Fore.LIGHTRED_EX) else: info_print("(✓) Download Completed =>", text=path, color=Fore.LIGHTGREEN_EX)
def dump(args: argparse.Namespace): targets = args.user count = args.count outfile = args.outfile if not targets: parser.error("at least one dump type must be specified") insta = load_obj() if not insta: err_print("No account logged in") return kwargs = {"count": count or 50} ex_kwargs = {"count": count or 50, "convert": False} jobs = [] for target in targets: if len(target) < 2: parser.error("illegal argument parsed in '{0}'".format(target)) arg = target[1:] if target[0] == "@": if args.likes or args.comments: parser.error("-likes, -comments: not allowed with @user type") if not args.followers and not args.followings: if args.count: parser.error("--count: not allowed with argument @user") jobs.append((insta.get_profile, (arg, ), {}, target, "User Information {0}")) else: if args.followers: jobs.append((insta.get_user_followers, (arg, ), ex_kwargs, target, "User Followers {0}")) if args.followings: jobs.append((insta.get_user_followings, (arg, ), ex_kwargs, target, "User Followings {0}")) elif target[0] == ":": if args.followers or args.followings: parser.error( "-followers, -followings: not allowed with :post type") if not args.likes and not args.comments: if args.count: parser.error("--count: not allowed with argument :post") jobs.append((insta.get_post, (arg, ), {}, target, "Post Information {0}")) else: if args.likes: jobs.append((insta.get_post_likes, (arg, ), ex_kwargs, target, "Post Likes {0}")) if args.comments: jobs.append((insta.get_post_comments, (arg, ), kwargs, target, "Post Comments {0}")) else: parser.error( "illegal symbol parsed in argument: '{0}'".format(target)) print(Fore.YELLOW + "Current User:"******"Count:", Style.BRIGHT + str(count or 50)) for i, (function, arguments, kwarguments, string, title) in enumerate(jobs, start=1): print() with handle_errors(current_function_name=function.__name__, is_final=i == len(jobs)): info_print("(Dump) {0}".format(function.__name__.title().replace( "_", " ")), text=string if string else None, color=Fore.LIGHTBLUE_EX) result = function(*arguments, **kwarguments) data = result.as_dict() if hasattr(result, "as_dict") else result if not data: info_print("(✗) Dump Failed", color=Fore.LIGHTRED_EX) return if outfile: # save to file path = os.path.abspath(outfile) if isinstance(data, dict): # => JSON with open(path, "w+") as f: json.dump(data, f, indent=4) else: # => txt with open(path, "w+") as f: buffer = [] for item in data: if isinstance(item, dict): # comments buffer.append(item) else: f.write("@" + str(item) + "\n") if buffer: json.dump(buffer, f, indent=4) # done info_print("(✓) Dump Succeeded =>", text=path, color=Fore.LIGHTGREEN_EX) else: # print to stdout pretty_print(data, title.format(string))