Example #1
0
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)
Example #2
0
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)
Example #3
0
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), ":")
Example #4
0
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)
Example #5
0
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)
Example #6
0
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))