Merge branch 'v4.1-dev'
This commit is contained in:
commit
5a7b76db31
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
instaloader [--comments] [--geotags] [--stories]
|
instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged]
|
||||||
[--login YOUR-USERNAME] [--fast-update]
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
profile | "#hashtag" | :stories | :feed | :saved
|
profile | "#hashtag" | :stories | :feed | :saved
|
||||||
|
|
||||||
|
8
docs/_static/style.css
vendored
8
docs/_static/style.css
vendored
@ -18,3 +18,11 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: #f48400;
|
color: #f48400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionmodified {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
@ -135,6 +135,17 @@ User Stories
|
|||||||
.. autoclass:: StoryItem
|
.. autoclass:: StoryItem
|
||||||
:no-show-inheritance:
|
:no-show-inheritance:
|
||||||
|
|
||||||
|
Highlights
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
.. autoclass:: Highlight
|
||||||
|
:no-show-inheritance:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
Bases: :class:`Story`
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
Profiles
|
Profiles
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
|
@ -60,12 +60,27 @@ already have a valid session cookie file.
|
|||||||
What to Download
|
What to Download
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. targets-start
|
||||||
|
|
||||||
Instaloader supports the following targets:
|
Instaloader supports the following targets:
|
||||||
|
|
||||||
- ``profile``
|
- ``profile``
|
||||||
Public profile, or private profile with :option:`--login`. For each profile
|
Public profile, or private profile with :option:`--login`.
|
||||||
you download, :option:`--stories` instructs Instaloader to also
|
|
||||||
**download the user's stories**.
|
If an already-downloaded profile has been renamed, Instaloader automatically
|
||||||
|
finds it by its unique ID and renames the folder accordingly.
|
||||||
|
|
||||||
|
Besides the profile's posts, its current profile picture is downloaded. For
|
||||||
|
each profile you download,
|
||||||
|
|
||||||
|
- :option:`--stories`
|
||||||
|
instructs Instaloader to also **download the user's stories**,
|
||||||
|
|
||||||
|
- :option:`--highlights`
|
||||||
|
to **download highlights of each profile that is downloaded**, and
|
||||||
|
|
||||||
|
- :option:`--tagged`
|
||||||
|
to **download posts where the user is tagged**.
|
||||||
|
|
||||||
- ``"#hashtag"``
|
- ``"#hashtag"``
|
||||||
Posts with a certain **hashtag** (the quotes are usually necessary),
|
Posts with a certain **hashtag** (the quotes are usually necessary),
|
||||||
@ -84,6 +99,14 @@ Instaloader supports the following targets:
|
|||||||
All profiles that are followed by ``profile``, i.e. the *followees* of
|
All profiles that are followed by ``profile``, i.e. the *followees* of
|
||||||
``profile`` (requires :option:`--login`).
|
``profile`` (requires :option:`--login`).
|
||||||
|
|
||||||
|
- ``-post``
|
||||||
|
The single **post** with the given shortcode. Must be preceeded by ``--`` in
|
||||||
|
the argument list to not be mistaken as an option flag.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. targets-end
|
||||||
|
|
||||||
Instaloader goes through all media matching the specified targets and
|
Instaloader goes through all media matching the specified targets and
|
||||||
downloads the pictures and videos and their captions. You can specify
|
downloads the pictures and videos and their captions. You can specify
|
||||||
|
|
||||||
|
@ -13,27 +13,41 @@ feed), ``:stories`` (stories of your followees) or ``:saved`` (collection of
|
|||||||
posts marked as saved).
|
posts marked as saved).
|
||||||
|
|
||||||
Here we explain the additional options that can be given to Instaloader to
|
Here we explain the additional options that can be given to Instaloader to
|
||||||
customize its behavior. To get a list of all flags, their abbreviations and
|
customize its behavior. For an
|
||||||
their descriptions, you may also run ``instaloader --help``. For an
|
|
||||||
introduction on how to use Instaloader, see
|
introduction on how to use Instaloader, see
|
||||||
:ref:`download-pictures-from-instagram`.
|
:ref:`download-pictures-from-instagram`.
|
||||||
|
|
||||||
What to Download
|
To get a list of all flags, their abbreviations and
|
||||||
^^^^^^^^^^^^^^^^
|
their descriptions, you may also run::
|
||||||
|
|
||||||
Specify a list of targets (profiles, #hashtags, ``:feed``, ``:stories`` or
|
instaloader --help
|
||||||
``:saved``). For each of these, Instaloader creates a folder and stores all
|
|
||||||
posts along with the pictures's captions and the current **profile picture**
|
|
||||||
there. If an already-downloaded profile has been renamed, Instaloader
|
|
||||||
automatically **finds it by its unique ID** and renames the folder likewise.
|
|
||||||
|
|
||||||
.. option:: --profile-pic-only, -P
|
Targets
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
Only download profile picture.
|
Specify a list of targets. For each of these, Instaloader creates a folder and
|
||||||
|
stores all posts along with the pictures's captions there.
|
||||||
|
|
||||||
.. option:: --no-profile-pic
|
.. include:: basic-usage.rst
|
||||||
|
:start-after: targets-start
|
||||||
|
:end-before: targets-end
|
||||||
|
|
||||||
Do not download profile picture.
|
- ``filename.json[.xz]``
|
||||||
|
Re-Download the given object
|
||||||
|
|
||||||
|
- ``+args.txt``
|
||||||
|
Read targets (and options) from given textfile. See :option:`+args.txt`.
|
||||||
|
|
||||||
|
What to Download of each Post
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --no-pictures
|
||||||
|
|
||||||
|
Do not download post pictures. Cannot be used together with
|
||||||
|
:option:`--fast-update`. Implies :option:`--no-video-thumbnails`, does not
|
||||||
|
imply :option:`--no-videos`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
.. option:: --no-videos, -V
|
.. option:: --no-videos, -V
|
||||||
|
|
||||||
@ -69,11 +83,6 @@ automatically **finds it by its unique ID** and renames the folder likewise.
|
|||||||
Template to write in txt file for each StoryItem. See
|
Template to write in txt file for each StoryItem. See
|
||||||
:ref:`metadata-text-files`.
|
:ref:`metadata-text-files`.
|
||||||
|
|
||||||
.. option:: --stories, -s
|
|
||||||
|
|
||||||
Also **download stories** of each profile that is downloaded. Requires
|
|
||||||
:option:`--login`.
|
|
||||||
|
|
||||||
.. option:: --no-metadata-json
|
.. option:: --no-metadata-json
|
||||||
|
|
||||||
Do not create a JSON file containing the metadata of each post.
|
Do not create a JSON file containing the metadata of each post.
|
||||||
@ -82,8 +91,50 @@ automatically **finds it by its unique ID** and renames the folder likewise.
|
|||||||
|
|
||||||
Do not xz compress JSON files, rather create pretty formatted JSONs.
|
Do not xz compress JSON files, rather create pretty formatted JSONs.
|
||||||
|
|
||||||
|
|
||||||
|
What to Download of each Profile
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --profile-pic-only, -P
|
||||||
|
|
||||||
|
.. deprecated:: 4.1
|
||||||
|
Use :option:`--no-posts`.
|
||||||
|
|
||||||
|
Only download profile picture.
|
||||||
|
|
||||||
|
.. option:: --no-posts
|
||||||
|
|
||||||
|
Do not download regular posts.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --no-profile-pic
|
||||||
|
|
||||||
|
Do not download profile picture.
|
||||||
|
|
||||||
|
.. option:: --stories, -s
|
||||||
|
|
||||||
|
Also **download stories** of each profile that is downloaded. Requires
|
||||||
|
:option:`--login`.
|
||||||
|
|
||||||
|
.. option:: --highlights
|
||||||
|
|
||||||
|
Also **download highlights** of each profile that is downloaded. Requires
|
||||||
|
:option:`--login`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --tagged
|
||||||
|
|
||||||
|
Also download posts where each profile is tagged.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
.. option:: --stories-only
|
.. option:: --stories-only
|
||||||
|
|
||||||
|
.. deprecated:: 4.1
|
||||||
|
Use :option:`--stories` :option:`--no-posts`.
|
||||||
|
|
||||||
Rather than downloading regular posts of each specified profile, only
|
Rather than downloading regular posts of each specified profile, only
|
||||||
download stories. Requires :option:`--login`. Does not imply
|
download stories. Requires :option:`--login`. Does not imply
|
||||||
:option:`--no-profile-pic`.
|
:option:`--no-profile-pic`.
|
||||||
@ -93,6 +144,15 @@ automatically **finds it by its unique ID** and renames the folder likewise.
|
|||||||
If possible, use ``:stories`` target rather than :option:`--stories-only`
|
If possible, use ``:stories`` target rather than :option:`--stories-only`
|
||||||
with all your followees. ``:stories`` uses fewer API requests.
|
with all your followees. ``:stories`` uses fewer API requests.
|
||||||
|
|
||||||
|
Which Posts to Download
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --fast-update, -F
|
||||||
|
|
||||||
|
For each target, stop when encountering the first already-downloaded picture.
|
||||||
|
This flag is recommended when you use Instaloader to update your personal
|
||||||
|
Instagram archive.
|
||||||
|
|
||||||
.. option:: --post-filter filter, --only-if filter
|
.. option:: --post-filter filter, --only-if filter
|
||||||
|
|
||||||
Expression that, if given, must evaluate to True for each post to be
|
Expression that, if given, must evaluate to True for each post to be
|
||||||
@ -108,20 +168,6 @@ automatically **finds it by its unique ID** and renames the folder likewise.
|
|||||||
evaluated to :class:`instaloader.StoryItem` attributes.
|
evaluated to :class:`instaloader.StoryItem` attributes.
|
||||||
See :ref:`filter-posts` for more examples.
|
See :ref:`filter-posts` for more examples.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
When to Stop Downloading
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
If none of these options are given, Instaloader goes through all pictures
|
|
||||||
matching the specified targets.
|
|
||||||
|
|
||||||
.. option:: --fast-update, -F
|
|
||||||
|
|
||||||
For each target, stop when encountering the first already-downloaded picture.
|
|
||||||
This flag is recommended when you use Instaloader to update your personal
|
|
||||||
Instagram archive.
|
|
||||||
|
|
||||||
.. option:: --count COUNT, -c
|
.. option:: --count COUNT, -c
|
||||||
|
|
||||||
Do not attempt to download more than COUNT posts. Applies only to
|
Do not attempt to download more than COUNT posts. Applies only to
|
||||||
@ -192,3 +238,21 @@ Miscellaneous Options
|
|||||||
Disable user interaction, i.e. do not print messages (except errors) and fail
|
Disable user interaction, i.e. do not print messages (except errors) and fail
|
||||||
if login credentials are needed but not given.
|
if login credentials are needed but not given.
|
||||||
This is handy for running :ref:`instaloader-as-cronjob`.
|
This is handy for running :ref:`instaloader-as-cronjob`.
|
||||||
|
|
||||||
|
.. option:: +args.txt
|
||||||
|
|
||||||
|
Read arguments from file `args.txt`, a shortcut to provide argument from
|
||||||
|
file rather than command-line. This provide a convient way to hide login
|
||||||
|
info from CLI. and also can use for simplify managment of long arguments.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
text file should separate arg with line break.
|
||||||
|
|
||||||
|
args.txt example::
|
||||||
|
|
||||||
|
--login MYUSENAME
|
||||||
|
--password MYPASSWORD
|
||||||
|
--fast-update
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
@ -41,7 +41,7 @@ See :ref:`install` for more options on how to install Instaloader.
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
instaloader [--comments] [--geotags] [--stories]
|
instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged]
|
||||||
[--login YOUR-USERNAME] [--fast-update]
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
profile | "#hashtag" |
|
profile | "#hashtag" |
|
||||||
:stories | :feed | :saved
|
:stories | :feed | :saved
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||||
|
|
||||||
|
|
||||||
__version__ = '4.0.8'
|
__version__ = '4.1rc1'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -15,5 +15,5 @@ else:
|
|||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .instaloader import Instaloader
|
from .instaloader import Instaloader
|
||||||
from .instaloadercontext import InstaloaderContext
|
from .instaloadercontext import InstaloaderContext
|
||||||
from .structures import (Post, PostSidecarNode, PostComment, PostLocation, Profile, Story, StoryItem,
|
from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostLocation, Profile, Story, StoryItem,
|
||||||
load_structure_from_file, save_structure_to_file)
|
load_structure_from_file, save_structure_to_file)
|
||||||
|
@ -18,7 +18,7 @@ def usage_string():
|
|||||||
argv0 = os.path.basename(sys.argv[0])
|
argv0 = os.path.basename(sys.argv[0])
|
||||||
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
||||||
return """
|
return """
|
||||||
{0} [--comments] [--geotags] [--stories]
|
{0} [--comments] [--geotags] [--stories] [--highlights] [--tagged]
|
||||||
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
||||||
{2:{1}} profile | "#hashtag" | :stories | :feed | :saved
|
{2:{1}} profile | "#hashtag" | :stories | :feed | :saved
|
||||||
{0} --help""".format(argv0, len(argv0), '')
|
{0} --help""".format(argv0, len(argv0), '')
|
||||||
@ -58,11 +58,11 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
|||||||
|
|
||||||
def _main(instaloader: Instaloader, targetlist: List[str],
|
def _main(instaloader: Instaloader, targetlist: List[str],
|
||||||
username: Optional[str] = None, password: Optional[str] = None,
|
username: Optional[str] = None, password: Optional[str] = None,
|
||||||
sessionfile: Optional[str] = None, max_count: Optional[int] = None,
|
sessionfile: Optional[str] = None,
|
||||||
profile_pic: bool = True, profile_pic_only: bool = False,
|
download_profile_pic: bool = True, download_posts=True,
|
||||||
|
download_stories: bool = False, download_highlights: bool = False, download_tagged: bool = False,
|
||||||
fast_update: bool = False,
|
fast_update: bool = False,
|
||||||
stories: bool = False, stories_only: bool = False,
|
max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
|
||||||
post_filter_str: Optional[str] = None,
|
|
||||||
storyitem_filter_str: Optional[str] = None) -> None:
|
storyitem_filter_str: Optional[str] = None) -> None:
|
||||||
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
|
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
|
||||||
# Parse and generate filter function
|
# Parse and generate filter function
|
||||||
@ -88,10 +88,6 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
else:
|
else:
|
||||||
instaloader.interactive_login(username)
|
instaloader.interactive_login(username)
|
||||||
instaloader.context.log("Logged in as %s." % username)
|
instaloader.context.log("Logged in as %s." % username)
|
||||||
# Determine what to download
|
|
||||||
download_profile_pic = profile_pic or profile_pic_only
|
|
||||||
download_profile_posts = not (stories_only or profile_pic_only)
|
|
||||||
download_profile_stories = stories or stories_only
|
|
||||||
# Try block for KeyboardInterrupt (save session on ^C)
|
# Try block for KeyboardInterrupt (save session on ^C)
|
||||||
profiles = set()
|
profiles = set()
|
||||||
anonymous_retry_profiles = set()
|
anonymous_retry_profiles = set()
|
||||||
@ -132,6 +128,8 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
elif target[0] == '#':
|
elif target[0] == '#':
|
||||||
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
|
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
|
||||||
post_filter=post_filter)
|
post_filter=post_filter)
|
||||||
|
elif target[0] == '-':
|
||||||
|
instaloader.download_post(Post.from_shortcode(instaloader.context, target[1:]), target)
|
||||||
elif target == ":feed":
|
elif target == ":feed":
|
||||||
instaloader.download_feed_posts(fast_update=fast_update, max_count=max_count,
|
instaloader.download_feed_posts(fast_update=fast_update, max_count=max_count,
|
||||||
post_filter=post_filter)
|
post_filter=post_filter)
|
||||||
@ -144,7 +142,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
try:
|
try:
|
||||||
profile = instaloader.check_profile_id(target)
|
profile = instaloader.check_profile_id(target)
|
||||||
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
||||||
if download_profile_pic or (download_profile_posts and not profile.is_private):
|
if download_profile_pic or ((download_posts or download_tagged) and not profile.is_private):
|
||||||
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
||||||
.format(target))
|
.format(target))
|
||||||
else:
|
else:
|
||||||
@ -154,37 +152,30 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
except ProfileNotExistsException as err:
|
except ProfileNotExistsException as err:
|
||||||
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
||||||
# check_profile_id() also does, since access to blocked profile may be responded with 404.
|
# check_profile_id() also does, since access to blocked profile may be responded with 404.
|
||||||
if instaloader.context.is_logged_in and (download_profile_pic or download_profile_posts):
|
if instaloader.context.is_logged_in and (download_profile_pic or download_posts or
|
||||||
|
download_tagged):
|
||||||
instaloader.context.log(err)
|
instaloader.context.log(err)
|
||||||
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
||||||
with instaloader.anonymous_copy() as anonymous_loader:
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
with instaloader.context.error_catcher():
|
with instaloader.context.error_catcher():
|
||||||
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target))
|
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target))
|
||||||
instaloader.context.log("Looks good.")
|
instaloader.context.error("Warning: {} will be downloaded anonymously (\"{}\")."
|
||||||
|
.format(target, err))
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if len(profiles) > 1:
|
if len(profiles) > 1:
|
||||||
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
|
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
|
||||||
' '.join([p.username for p in profiles])))
|
' '.join([p.username for p in profiles])))
|
||||||
if download_profile_pic or download_profile_posts:
|
instaloader.download_profiles(profiles,
|
||||||
# Iterate through profiles list and download them
|
download_profile_pic, download_posts, download_tagged, download_highlights,
|
||||||
for target in profiles:
|
download_stories, fast_update, post_filter, storyitem_filter)
|
||||||
with instaloader.context.error_catcher(target):
|
if anonymous_retry_profiles:
|
||||||
instaloader.download_profile(target, download_profile_pic, not download_profile_posts,
|
instaloader.context.log("Downloading anonymously: {}"
|
||||||
fast_update, post_filter=post_filter)
|
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
||||||
if anonymous_retry_profiles:
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
instaloader.context.log("Downloading anonymously: {}"
|
anonymous_loader.download_profiles(anonymous_retry_profiles,
|
||||||
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
download_profile_pic, download_posts, download_tagged,
|
||||||
with instaloader.anonymous_copy() as anonymous_loader:
|
fast_update=fast_update, post_filter=post_filter)
|
||||||
for target in anonymous_retry_profiles:
|
|
||||||
with instaloader.context.error_catcher(target):
|
|
||||||
anonymous_loader.download_profile(target, download_profile_pic, not download_profile_posts,
|
|
||||||
fast_update, post_filter=post_filter)
|
|
||||||
if download_profile_stories and profiles:
|
|
||||||
with instaloader.context.error_catcher("Download stories"):
|
|
||||||
instaloader.context.log("Downloading stories")
|
|
||||||
instaloader.download_stories(userids=list(profiles), fast_update=fast_update,
|
|
||||||
filename_target=None, storyitem_filter=storyitem_filter)
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nInterrupted by user.", file=sys.stderr)
|
print("\nInterrupted by user.", file=sys.stderr)
|
||||||
# Save session if it is useful
|
# Save session if it is useful
|
||||||
@ -204,72 +195,95 @@ def main():
|
|||||||
parser = ArgumentParser(description=__doc__, add_help=False, usage=usage_string(),
|
parser = ArgumentParser(description=__doc__, add_help=False, usage=usage_string(),
|
||||||
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
|
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
|
||||||
"The complete documentation can be found at "
|
"The complete documentation can be found at "
|
||||||
"https://instaloader.github.io/.")
|
"https://instaloader.github.io/.",
|
||||||
|
fromfile_prefix_chars='+')
|
||||||
|
|
||||||
g_what = parser.add_argument_group('What to Download',
|
g_targets = parser.add_argument_group("What to Download",
|
||||||
'Specify a list of profiles or #hashtags. For each of these, Instaloader '
|
"Specify a list of targets. For each of these, Instaloader creates a folder "
|
||||||
'creates a folder and '
|
"and downloads all posts. The following targets are supported:")
|
||||||
'downloads all posts along with the pictures\'s '
|
g_targets.add_argument('profile', nargs='*',
|
||||||
'captions and the current profile picture. '
|
help="Download profile. If an already-downloaded profile has been renamed, Instaloader "
|
||||||
'If an already-downloaded profile has been renamed, Instaloader automatically '
|
"automatically finds it by its unique ID and renames the folder likewise.")
|
||||||
'finds it by its unique ID and renames the folder likewise.')
|
g_targets.add_argument('_at_profile', nargs='*', metavar="@profile",
|
||||||
g_what.add_argument('profile', nargs='*', metavar='profile|#hashtag',
|
help="Download all followees of profile. Requires --login. "
|
||||||
help='Name of profile or #hashtag to download. '
|
"Consider using :feed rather than @yourself.")
|
||||||
'Alternatively, if --login is given: @<profile> to download all followees of '
|
g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.")
|
||||||
'<profile>; the special targets '
|
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
|
||||||
':feed to download pictures from your feed; '
|
help="Download pictures from your feed. Requires --login.")
|
||||||
':stories to download the stories of your followees; or '
|
g_targets.add_argument('_stories', nargs='*', metavar=":stories",
|
||||||
':saved to download the posts marked as saved.')
|
help="Download the stories of your followees. Requires --login.")
|
||||||
g_what.add_argument('-P', '--profile-pic-only', action='store_true',
|
g_targets.add_argument('_saved', nargs='*', metavar=":saved",
|
||||||
help='Only download profile picture.')
|
help="Download the posts that you marked as saved. Requires --login.")
|
||||||
g_what.add_argument('--no-profile-pic', action='store_true',
|
g_targets.add_argument('_singlepost', nargs='*', metavar="-- -shortcode",
|
||||||
|
help="Download the post with the given shortcode")
|
||||||
|
g_targets.add_argument('_json', nargs='*', metavar="filename.json[.xz]",
|
||||||
|
help="Re-Download the given object.")
|
||||||
|
g_targets.add_argument('_fromfile', nargs='*', metavar="+args.txt",
|
||||||
|
help="Read targets (and options) from given textfile.")
|
||||||
|
|
||||||
|
g_post = parser.add_argument_group("What to Download of each Post")
|
||||||
|
|
||||||
|
g_prof = parser.add_argument_group("What to Download of each Profile")
|
||||||
|
|
||||||
|
g_prof.add_argument('-P', '--profile-pic-only', action='store_true',
|
||||||
|
help=SUPPRESS)
|
||||||
|
g_prof.add_argument('--no-posts', action='store_true',
|
||||||
|
help="Do not download regular posts.")
|
||||||
|
g_prof.add_argument('--no-profile-pic', action='store_true',
|
||||||
help='Do not download profile picture.')
|
help='Do not download profile picture.')
|
||||||
g_what.add_argument('-V', '--no-videos', action='store_true',
|
g_post.add_argument('--no-pictures', action='store_true',
|
||||||
|
help='Do not download post pictures. Cannot be used together with --fast-update. '
|
||||||
|
'Implies --no-video-thumbnails, does not imply --no-videos.')
|
||||||
|
g_post.add_argument('-V', '--no-videos', action='store_true',
|
||||||
help='Do not download videos.')
|
help='Do not download videos.')
|
||||||
g_what.add_argument('--no-video-thumbnails', action='store_true',
|
g_post.add_argument('--no-video-thumbnails', action='store_true',
|
||||||
help='Do not download thumbnails of videos.')
|
help='Do not download thumbnails of videos.')
|
||||||
g_what.add_argument('-G', '--geotags', action='store_true',
|
g_post.add_argument('-G', '--geotags', action='store_true',
|
||||||
help='Download geotags when available. Geotags are stored as a '
|
help='Download geotags when available. Geotags are stored as a '
|
||||||
'text file with the location\'s name and a Google Maps link. '
|
'text file with the location\'s name and a Google Maps link. '
|
||||||
'This requires an additional request to the Instagram '
|
'This requires an additional request to the Instagram '
|
||||||
'server for each picture, which is why it is disabled by default.')
|
'server for each picture, which is why it is disabled by default.')
|
||||||
g_what.add_argument('-C', '--comments', action='store_true',
|
g_post.add_argument('-C', '--comments', action='store_true',
|
||||||
help='Download and update comments for each post. '
|
help='Download and update comments for each post. '
|
||||||
'This requires an additional request to the Instagram '
|
'This requires an additional request to the Instagram '
|
||||||
'server for each post, which is why it is disabled by default.')
|
'server for each post, which is why it is disabled by default.')
|
||||||
g_what.add_argument('--no-captions', action='store_true',
|
g_post.add_argument('--no-captions', action='store_true',
|
||||||
help='Do not create txt files.')
|
help='Do not create txt files.')
|
||||||
g_what.add_argument('--post-metadata-txt', action='append',
|
g_post.add_argument('--post-metadata-txt', action='append',
|
||||||
help='Template to write in txt file for each Post.')
|
help='Template to write in txt file for each Post.')
|
||||||
g_what.add_argument('--storyitem-metadata-txt', action='append',
|
g_post.add_argument('--storyitem-metadata-txt', action='append',
|
||||||
help='Template to write in txt file for each StoryItem.')
|
help='Template to write in txt file for each StoryItem.')
|
||||||
g_what.add_argument('--no-metadata-json', action='store_true',
|
g_post.add_argument('--no-metadata-json', action='store_true',
|
||||||
help='Do not create a JSON file containing the metadata of each post.')
|
help='Do not create a JSON file containing the metadata of each post.')
|
||||||
g_what.add_argument('--metadata-json', action='store_true',
|
g_post.add_argument('--metadata-json', action='store_true',
|
||||||
help=SUPPRESS)
|
help=SUPPRESS)
|
||||||
g_what.add_argument('--no-compress-json', action='store_true',
|
g_post.add_argument('--no-compress-json', action='store_true',
|
||||||
help='Do not xz compress JSON files, rather create pretty formatted JSONs.')
|
help='Do not xz compress JSON files, rather create pretty formatted JSONs.')
|
||||||
g_what.add_argument('-s', '--stories', action='store_true',
|
g_prof.add_argument('-s', '--stories', action='store_true',
|
||||||
help='Also download stories of each profile that is downloaded. Requires --login.')
|
help='Also download stories of each profile that is downloaded. Requires --login.')
|
||||||
g_what.add_argument('--stories-only', action='store_true',
|
g_prof.add_argument('--stories-only', action='store_true',
|
||||||
help='Rather than downloading regular posts of each specified profile, only download '
|
help=SUPPRESS)
|
||||||
'stories. Requires --login. Does not imply --no-profile-pic.')
|
g_prof.add_argument('--highlights', action='store_true',
|
||||||
g_what.add_argument('--post-filter', '--only-if', metavar='filter',
|
help='Also download highlights of each profile that is downloaded. Requires --login.')
|
||||||
|
g_prof.add_argument('--tagged', action='store_true',
|
||||||
|
help='Also download posts where each profile is tagged.')
|
||||||
|
|
||||||
|
g_cond = parser.add_argument_group("Which Posts to Download")
|
||||||
|
|
||||||
|
g_cond.add_argument('-F', '--fast-update', action='store_true',
|
||||||
|
help='For each target, stop when encountering the first already-downloaded picture. This '
|
||||||
|
'flag is recommended when you use Instaloader to update your personal Instagram archive.')
|
||||||
|
|
||||||
|
g_cond.add_argument('--post-filter', '--only-if', metavar='filter',
|
||||||
help='Expression that, if given, must evaluate to True for each post to be downloaded. Must be '
|
help='Expression that, if given, must evaluate to True for each post to be downloaded. Must be '
|
||||||
'a syntactically valid python expression. Variables are evaluated to '
|
'a syntactically valid python expression. Variables are evaluated to '
|
||||||
'instaloader.Post attributes. Example: --post-filter=viewer_has_liked.')
|
'instaloader.Post attributes. Example: --post-filter=viewer_has_liked.')
|
||||||
g_what.add_argument('--storyitem-filter', metavar='filter',
|
g_cond.add_argument('--storyitem-filter', metavar='filter',
|
||||||
help='Expression that, if given, must evaluate to True for each storyitem to be downloaded. '
|
help='Expression that, if given, must evaluate to True for each storyitem to be downloaded. '
|
||||||
'Must be a syntactically valid python expression. Variables are evaluated to '
|
'Must be a syntactically valid python expression. Variables are evaluated to '
|
||||||
'instaloader.StoryItem attributes.')
|
'instaloader.StoryItem attributes.')
|
||||||
|
|
||||||
g_stop = parser.add_argument_group('When to Stop Downloading',
|
g_cond.add_argument('-c', '--count',
|
||||||
'If none of these options are given, Instaloader goes through all pictures '
|
|
||||||
'matching the specified targets.')
|
|
||||||
g_stop.add_argument('-F', '--fast-update', action='store_true',
|
|
||||||
help='For each target, stop when encountering the first already-downloaded picture. This '
|
|
||||||
'flag is recommended when you use Instaloader to update your personal Instagram archive.')
|
|
||||||
g_stop.add_argument('-c', '--count',
|
|
||||||
help='Do not attempt to download more than COUNT posts. '
|
help='Do not attempt to download more than COUNT posts. '
|
||||||
'Applies only to #hashtag and :feed.')
|
'Applies only to #hashtag and :feed.')
|
||||||
|
|
||||||
@ -339,8 +353,17 @@ def main():
|
|||||||
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; "
|
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; "
|
||||||
"That contradicts.")
|
"That contradicts.")
|
||||||
|
|
||||||
|
if args.no_pictures and args.fast_update:
|
||||||
|
raise SystemExit('--no-pictures and --fast-update cannot be used together.')
|
||||||
|
|
||||||
|
# Determine what to download
|
||||||
|
download_profile_pic = not args.no_profile_pic or args.profile_pic_only
|
||||||
|
download_posts = not (args.no_posts or args.stories_only or args.profile_pic_only)
|
||||||
|
download_stories = args.stories or args.stories_only
|
||||||
|
|
||||||
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, user_agent=args.user_agent,
|
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, user_agent=args.user_agent,
|
||||||
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern,
|
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern,
|
||||||
|
download_pictures=not args.no_pictures,
|
||||||
download_videos=not args.no_videos, download_video_thumbnails=not args.no_video_thumbnails,
|
download_videos=not args.no_videos, download_video_thumbnails=not args.no_video_thumbnails,
|
||||||
download_geotags=args.geotags,
|
download_geotags=args.geotags,
|
||||||
download_comments=args.comments, save_metadata=not args.no_metadata_json,
|
download_comments=args.comments, save_metadata=not args.no_metadata_json,
|
||||||
@ -354,12 +377,13 @@ def main():
|
|||||||
username=args.login.lower() if args.login is not None else None,
|
username=args.login.lower() if args.login is not None else None,
|
||||||
password=args.password,
|
password=args.password,
|
||||||
sessionfile=args.sessionfile,
|
sessionfile=args.sessionfile,
|
||||||
max_count=int(args.count) if args.count is not None else None,
|
download_profile_pic=download_profile_pic,
|
||||||
profile_pic=not args.no_profile_pic,
|
download_posts=download_posts,
|
||||||
profile_pic_only=args.profile_pic_only,
|
download_stories=download_stories,
|
||||||
|
download_highlights=args.highlights,
|
||||||
|
download_tagged=args.tagged,
|
||||||
fast_update=args.fast_update,
|
fast_update=args.fast_update,
|
||||||
stories=args.stories,
|
max_count=int(args.count) if args.count is not None else None,
|
||||||
stories_only=args.stories_only,
|
|
||||||
post_filter_str=args.post_filter,
|
post_filter_str=args.post_filter,
|
||||||
storyitem_filter_str=args.storyitem_filter)
|
storyitem_filter_str=args.storyitem_filter)
|
||||||
loader.close()
|
loader.close()
|
||||||
|
@ -11,11 +11,11 @@ from contextlib import contextmanager, suppress
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Callable, Iterator, List, Optional, Union
|
from typing import Any, Callable, Iterator, List, Optional, Set, Union
|
||||||
|
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .instaloadercontext import InstaloaderContext
|
from .instaloadercontext import InstaloaderContext
|
||||||
from .structures import JsonExportable, Post, PostLocation, Profile, Story, StoryItem, save_structure_to_file
|
from .structures import Highlight, JsonExportable, Post, PostLocation, Profile, Story, StoryItem, save_structure_to_file
|
||||||
|
|
||||||
|
|
||||||
def get_default_session_filename(username: str) -> str:
|
def get_default_session_filename(username: str) -> str:
|
||||||
@ -70,7 +70,10 @@ class _PostPathFormatter(_ArbitraryItemFormatter):
|
|||||||
def vformat(self, format_string, args, kwargs):
|
def vformat(self, format_string, args, kwargs):
|
||||||
"""Override :meth:`string.Formatter.vformat` for character substitution in paths for Windows, see issue #84."""
|
"""Override :meth:`string.Formatter.vformat` for character substitution in paths for Windows, see issue #84."""
|
||||||
ret = super().vformat(format_string, args, kwargs)
|
ret = super().vformat(format_string, args, kwargs)
|
||||||
return ret.replace(':', '\ua789') if platform.system() == 'Windows' else ret
|
if platform.system() == 'Windows':
|
||||||
|
ret = ret.replace(':', '\ua789').replace('<', '\ufe64').replace('>', '\ufe65').replace('\"', '\uff02')
|
||||||
|
ret = ret.replace('\\', '\uff3c').replace('|', '\uff5c').replace('?', '\ufe16').replace('*', '\uff0a')
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class Instaloader:
|
class Instaloader:
|
||||||
@ -80,6 +83,7 @@ class Instaloader:
|
|||||||
:param user_agent: :option:`--user-agent`
|
:param user_agent: :option:`--user-agent`
|
||||||
:param dirname_pattern: :option:`--dirname-pattern`, default is ``{target}``
|
:param dirname_pattern: :option:`--dirname-pattern`, default is ``{target}``
|
||||||
:param filename_pattern: :option:`--filename-pattern`, default is ``{date_utc}_UTC``
|
:param filename_pattern: :option:`--filename-pattern`, default is ``{date_utc}_UTC``
|
||||||
|
:param download_pictures: not :option:`--no-pictures`
|
||||||
:param download_videos: not :option:`--no-videos`
|
:param download_videos: not :option:`--no-videos`
|
||||||
:param download_video_thumbnails: not :option:`--no-video-thumbnails`
|
:param download_video_thumbnails: not :option:`--no-video-thumbnails`
|
||||||
:param download_geotags: :option:`--geotags`
|
:param download_geotags: :option:`--geotags`
|
||||||
@ -102,6 +106,7 @@ class Instaloader:
|
|||||||
user_agent: Optional[str] = None,
|
user_agent: Optional[str] = None,
|
||||||
dirname_pattern: Optional[str] = None,
|
dirname_pattern: Optional[str] = None,
|
||||||
filename_pattern: Optional[str] = None,
|
filename_pattern: Optional[str] = None,
|
||||||
|
download_pictures=True,
|
||||||
download_videos: bool = True,
|
download_videos: bool = True,
|
||||||
download_video_thumbnails: bool = True,
|
download_video_thumbnails: bool = True,
|
||||||
download_geotags: bool = True,
|
download_geotags: bool = True,
|
||||||
@ -118,6 +123,7 @@ class Instaloader:
|
|||||||
# configuration parameters
|
# configuration parameters
|
||||||
self.dirname_pattern = dirname_pattern or "{target}"
|
self.dirname_pattern = dirname_pattern or "{target}"
|
||||||
self.filename_pattern = filename_pattern or "{date_utc}_UTC"
|
self.filename_pattern = filename_pattern or "{date_utc}_UTC"
|
||||||
|
self.download_pictures = download_pictures
|
||||||
self.download_videos = download_videos
|
self.download_videos = download_videos
|
||||||
self.download_video_thumbnails = download_video_thumbnails
|
self.download_video_thumbnails = download_video_thumbnails
|
||||||
self.download_geotags = download_geotags
|
self.download_geotags = download_geotags
|
||||||
@ -133,11 +139,15 @@ class Instaloader:
|
|||||||
def anonymous_copy(self):
|
def anonymous_copy(self):
|
||||||
"""Yield an anonymous, otherwise equally-configured copy of an Instaloader instance; Then copy its error log."""
|
"""Yield an anonymous, otherwise equally-configured copy of an Instaloader instance; Then copy its error log."""
|
||||||
new_loader = Instaloader(self.context.sleep, self.context.quiet, self.context.user_agent, self.dirname_pattern,
|
new_loader = Instaloader(self.context.sleep, self.context.quiet, self.context.user_agent, self.dirname_pattern,
|
||||||
self.filename_pattern, self.download_videos, self.download_video_thumbnails,
|
self.filename_pattern, download_pictures=self.download_pictures,
|
||||||
self.download_geotags, self.download_comments, self.save_metadata,
|
download_videos=self.download_videos,
|
||||||
self.compress_json, self.post_metadata_txt_pattern,
|
download_video_thumbnails=self.download_video_thumbnails,
|
||||||
self.storyitem_metadata_txt_pattern, self.context.graphql_count_per_slidingwindow,
|
download_geotags=self.download_geotags, download_comments=self.download_comments,
|
||||||
self.context.max_connection_attempts)
|
save_metadata=self.save_metadata, compress_json=self.compress_json,
|
||||||
|
post_metadata_txt_pattern=self.post_metadata_txt_pattern,
|
||||||
|
storyitem_metadata_txt_pattern=self.storyitem_metadata_txt_pattern,
|
||||||
|
graphql_rate_limit=self.context.graphql_count_per_slidingwindow,
|
||||||
|
max_connection_attempts=self.context.max_connection_attempts)
|
||||||
new_loader.context.query_timestamps = self.context.query_timestamps
|
new_loader.context.query_timestamps = self.context.query_timestamps
|
||||||
yield new_loader
|
yield new_loader
|
||||||
self.context.error_log.extend(new_loader.context.error_log)
|
self.context.error_log.extend(new_loader.context.error_log)
|
||||||
@ -328,6 +338,12 @@ class Instaloader:
|
|||||||
:raises ConnectionException: If connection to Instagram failed."""
|
:raises ConnectionException: If connection to Instagram failed."""
|
||||||
self.context.login(user, passwd)
|
self.context.login(user, passwd)
|
||||||
|
|
||||||
|
def format_filename(self, item: Union[Post, StoryItem], target: Optional[str] = None):
|
||||||
|
"""Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1"""
|
||||||
|
return _PostPathFormatter(item).format(self.filename_pattern, target=target)
|
||||||
|
|
||||||
def download_post(self, post: Post, target: str) -> bool:
|
def download_post(self, post: Post, target: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Download everything associated with one instagram post node, i.e. picture, caption and video.
|
Download everything associated with one instagram post node, i.e. picture, caption and video.
|
||||||
@ -338,30 +354,31 @@ class Instaloader:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target)
|
dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target)
|
||||||
filename = dirname + '/' + _PostPathFormatter(post).format(self.filename_pattern, target=target)
|
filename = dirname + '/' + self.format_filename(post, target=target)
|
||||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
|
|
||||||
# Download the image(s) / video thumbnail and videos within sidecars if desired
|
# Download the image(s) / video thumbnail and videos within sidecars if desired
|
||||||
downloaded = False
|
downloaded = False
|
||||||
if post.typename == 'GraphSidecar':
|
if self.download_pictures:
|
||||||
edge_number = 1
|
if post.typename == 'GraphSidecar':
|
||||||
for sidecar_node in post.get_sidecar_nodes():
|
edge_number = 1
|
||||||
# Download picture or video thumbnail
|
for sidecar_node in post.get_sidecar_nodes():
|
||||||
if not sidecar_node.is_video or self.download_video_thumbnails is True:
|
# Download picture or video thumbnail
|
||||||
downloaded |= self.download_pic(filename=filename, url=sidecar_node.display_url,
|
if not sidecar_node.is_video or self.download_video_thumbnails is True:
|
||||||
mtime=post.date_local, filename_suffix=str(edge_number))
|
downloaded |= self.download_pic(filename=filename, url=sidecar_node.display_url,
|
||||||
# Additionally download video if available and desired
|
mtime=post.date_local, filename_suffix=str(edge_number))
|
||||||
if sidecar_node.is_video and self.download_videos is True:
|
# Additionally download video if available and desired
|
||||||
downloaded |= self.download_pic(filename=filename, url=sidecar_node.video_url,
|
if sidecar_node.is_video and self.download_videos is True:
|
||||||
mtime=post.date_local, filename_suffix=str(edge_number))
|
downloaded |= self.download_pic(filename=filename, url=sidecar_node.video_url,
|
||||||
edge_number += 1
|
mtime=post.date_local, filename_suffix=str(edge_number))
|
||||||
elif post.typename == 'GraphImage':
|
edge_number += 1
|
||||||
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local)
|
elif post.typename == 'GraphImage':
|
||||||
elif post.typename == 'GraphVideo':
|
|
||||||
if self.download_video_thumbnails is True:
|
|
||||||
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local)
|
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local)
|
||||||
else:
|
elif post.typename == 'GraphVideo':
|
||||||
self.context.error("Warning: {0} has unknown typename: {1}".format(post, post.typename))
|
if self.download_video_thumbnails is True:
|
||||||
|
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local)
|
||||||
|
else:
|
||||||
|
self.context.error("Warning: {0} has unknown typename: {1}".format(post, post.typename))
|
||||||
|
|
||||||
# Save caption if desired
|
# Save caption if desired
|
||||||
metadata_string = _ArbitraryItemFormatter(post).format(self.post_metadata_txt_pattern).strip()
|
metadata_string = _ArbitraryItemFormatter(post).format(self.post_metadata_txt_pattern).strip()
|
||||||
@ -462,7 +479,7 @@ class Instaloader:
|
|||||||
|
|
||||||
date_local = item.date_local
|
date_local = item.date_local
|
||||||
dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target)
|
dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target)
|
||||||
filename = dirname + '/' + _PostPathFormatter(item).format(self.filename_pattern, target=target)
|
filename = dirname + '/' + self.format_filename(item, target=target)
|
||||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
downloaded = False
|
downloaded = False
|
||||||
if not item.is_video or self.download_video_thumbnails is True:
|
if not item.is_video or self.download_video_thumbnails is True:
|
||||||
@ -480,6 +497,62 @@ class Instaloader:
|
|||||||
self.context.log()
|
self.context.log()
|
||||||
return downloaded
|
return downloaded
|
||||||
|
|
||||||
|
@_requires_login
|
||||||
|
def get_highlights(self, user: Union[int, Profile]) -> Iterator[Highlight]:
|
||||||
|
"""Get all highlights from a user.
|
||||||
|
To use this, one needs to be logged in.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
:param user: ID or Profile of the user whose highlights should get fetched.
|
||||||
|
"""
|
||||||
|
|
||||||
|
userid = user if isinstance(user, int) else user.userid
|
||||||
|
data = self.context.graphql_query("7c16654f22c819fb63d1183034a5162f",
|
||||||
|
{"user_id": userid, "include_chaining": False, "include_reel": False,
|
||||||
|
"include_suggested_users": False, "include_logged_out_extras": False,
|
||||||
|
"include_highlight_reels": True})["data"]["user"]['edge_highlight_reels']
|
||||||
|
if data is None:
|
||||||
|
raise BadResponseException('Bad highlights reel JSON.')
|
||||||
|
yield from (Highlight(self.context, edge['node'], user if isinstance(user, Profile) else None)
|
||||||
|
for edge in data['edges'])
|
||||||
|
|
||||||
|
@_requires_login
|
||||||
|
def download_highlights(self,
|
||||||
|
user: Union[int, Profile],
|
||||||
|
fast_update: bool = False,
|
||||||
|
filename_target: Optional[str] = None,
|
||||||
|
storyitem_filter: Optional[Callable[[StoryItem], bool]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Download available highlights from a user whose ID is given.
|
||||||
|
To use this, one needs to be logged in.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
:param user: ID or Profile of the user whose highlights should get downloaded.
|
||||||
|
:param fast_update: If true, abort when first already-downloaded picture is encountered
|
||||||
|
:param filename_target: Replacement for {target} in dirname_pattern and filename_pattern
|
||||||
|
or None if profile name and the highlights' titles should be used instead
|
||||||
|
:param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded
|
||||||
|
"""
|
||||||
|
for user_highlight in self.get_highlights(user):
|
||||||
|
name = user_highlight.owner_username
|
||||||
|
self.context.log("Retrieving highlights \"{}\" from profile {}".format(user_highlight.title, name))
|
||||||
|
totalcount = user_highlight.itemcount
|
||||||
|
count = 1
|
||||||
|
for item in user_highlight.get_items():
|
||||||
|
if storyitem_filter is not None and not storyitem_filter(item):
|
||||||
|
self.context.log("<{} skipped>".format(item), flush=True)
|
||||||
|
continue
|
||||||
|
self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
|
||||||
|
count += 1
|
||||||
|
with self.context.error_catcher('Download highlights \"{}\" from user {}'.format(user_highlight.title, name)):
|
||||||
|
downloaded = self.download_storyitem(item, filename_target
|
||||||
|
if filename_target
|
||||||
|
else '{}/{}'.format(name, user_highlight.title))
|
||||||
|
if fast_update and not downloaded:
|
||||||
|
break
|
||||||
|
|
||||||
@_requires_login
|
@_requires_login
|
||||||
def get_feed_posts(self) -> Iterator[Post]:
|
def get_feed_posts(self) -> Iterator[Post]:
|
||||||
"""Get Posts of the user's feed.
|
"""Get Posts of the user's feed.
|
||||||
@ -619,6 +692,26 @@ class Instaloader:
|
|||||||
if fast_update and not downloaded:
|
if fast_update and not downloaded:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def download_tagged(self, profile: Profile, fast_update: bool = False,
|
||||||
|
target: Optional[str] = None,
|
||||||
|
post_filter: Optional[Callable[[Post], bool]] = None) -> None:
|
||||||
|
"""Download all posts where a profile is tagged.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1"""
|
||||||
|
if target is None:
|
||||||
|
target = profile.username + '/:tagged'
|
||||||
|
self.context.log("Retrieving tagged posts for profile {}.".format(profile.username))
|
||||||
|
count = 1
|
||||||
|
for post in profile.get_tagged_posts():
|
||||||
|
self.context.log("[%3i/???] " % (count), end="", flush=True)
|
||||||
|
count += 1
|
||||||
|
if post_filter is not None and not post_filter(post):
|
||||||
|
self.context.log('<{} skipped>'.format(post))
|
||||||
|
with self.context.error_catcher('Download tagged {}'.format(profile.username)):
|
||||||
|
downloaded = self.download_post(post, target)
|
||||||
|
if fast_update and not downloaded:
|
||||||
|
break
|
||||||
|
|
||||||
def _get_id_filename(self, profile_name: str) -> str:
|
def _get_id_filename(self, profile_name: str) -> str:
|
||||||
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
||||||
format_string_contains_key(self.dirname_pattern, 'target'))):
|
format_string_contains_key(self.dirname_pattern, 'target'))):
|
||||||
@ -630,6 +723,8 @@ class Instaloader:
|
|||||||
def save_profile_id(self, profile: Profile):
|
def save_profile_id(self, profile: Profile):
|
||||||
"""
|
"""
|
||||||
Store ID of profile locally.
|
Store ID of profile locally.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0.6
|
||||||
"""
|
"""
|
||||||
os.makedirs(self.dirname_pattern.format(profile=profile.username,
|
os.makedirs(self.dirname_pattern.format(profile=profile.username,
|
||||||
target=profile.username), exist_ok=True)
|
target=profile.username), exist_ok=True)
|
||||||
@ -682,13 +777,102 @@ class Instaloader:
|
|||||||
return profile
|
return profile
|
||||||
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile_name))
|
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile_name))
|
||||||
|
|
||||||
|
def download_profiles(self, profiles: Set[Profile],
|
||||||
|
profile_pic: bool = True, posts: bool = True,
|
||||||
|
tagged: bool = False, highlights: bool = False, stories: bool = False,
|
||||||
|
fast_update: bool = False,
|
||||||
|
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
|
storyitem_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
|
raise_errors: bool = False):
|
||||||
|
"""High-level method to download set of profiles.
|
||||||
|
|
||||||
|
:param profiles: Set of profiles to download.
|
||||||
|
:param profile_pic: not :option:`--no-profile-pic`.
|
||||||
|
:param posts: not :option:`--no-posts`.
|
||||||
|
:param tagged: :option:`--tagged`.
|
||||||
|
:param highlights: :option:`--highlights`.
|
||||||
|
:param stories: :option:`--stories`.
|
||||||
|
:param fast_update: :option:`--fast-update`.
|
||||||
|
:param post_filter: :option:`--post-filter`.
|
||||||
|
:param storyitem_filter: :option:`--post-filter`.
|
||||||
|
:param raise_errors:
|
||||||
|
Whether :exc:`LoginRequiredException` and :exc:`PrivateProfileNotFollowedException` should be raised or
|
||||||
|
catched and printed with :meth:`InstaloaderContext.error_catcher`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1"""
|
||||||
|
|
||||||
|
def _error_raiser(_str):
|
||||||
|
yield
|
||||||
|
|
||||||
|
error_handler = _error_raiser if raise_errors else self.context.error_catcher
|
||||||
|
|
||||||
|
for profile in profiles:
|
||||||
|
with error_handler(profile.username):
|
||||||
|
profile_name = profile.username
|
||||||
|
|
||||||
|
# Save metadata as JSON if desired.
|
||||||
|
if self.save_metadata:
|
||||||
|
json_filename = '{0}/{1}_{2}'.format(self.dirname_pattern.format(profile=profile_name,
|
||||||
|
target=profile_name),
|
||||||
|
profile_name, profile.userid)
|
||||||
|
self.save_metadata_json(json_filename, profile)
|
||||||
|
|
||||||
|
# Download profile picture
|
||||||
|
if profile_pic:
|
||||||
|
with self.context.error_catcher('Download profile picture of {}'.format(profile_name)):
|
||||||
|
self.download_profilepic(profile)
|
||||||
|
|
||||||
|
# Catch some errors
|
||||||
|
if profile.is_private:
|
||||||
|
if not self.context.is_logged_in:
|
||||||
|
raise LoginRequiredException("--login=USERNAME required.")
|
||||||
|
if not profile.followed_by_viewer and self.context.username != profile.username:
|
||||||
|
raise PrivateProfileNotFollowedException("Private but not followed.")
|
||||||
|
|
||||||
|
# Download tagged, if requested
|
||||||
|
if tagged:
|
||||||
|
with self.context.error_catcher('Download tagged of {}'.format(profile_name)):
|
||||||
|
self.download_tagged(profile, fast_update=fast_update, post_filter=post_filter)
|
||||||
|
|
||||||
|
# Download highlights, if requested
|
||||||
|
if highlights:
|
||||||
|
with self.context.error_catcher('Download highlights of {}'.format(profile_name)):
|
||||||
|
self.download_highlights(profile, fast_update=fast_update, storyitem_filter=storyitem_filter)
|
||||||
|
|
||||||
|
# Iterate over pictures and download them
|
||||||
|
if posts:
|
||||||
|
self.context.log("Retrieving posts from profile {}.".format(profile_name))
|
||||||
|
totalcount = profile.mediacount
|
||||||
|
count = 1
|
||||||
|
for post in profile.get_posts():
|
||||||
|
self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
|
||||||
|
count += 1
|
||||||
|
if post_filter is not None and not post_filter(post):
|
||||||
|
self.context.log('<skipped>')
|
||||||
|
continue
|
||||||
|
with self.context.error_catcher("Download {} of {}".format(post, profile_name)):
|
||||||
|
downloaded = self.download_post(post, target=profile_name)
|
||||||
|
if fast_update and not downloaded:
|
||||||
|
break
|
||||||
|
|
||||||
|
if stories and profiles:
|
||||||
|
with self.context.error_catcher("Download stories"):
|
||||||
|
self.context.log("Downloading stories")
|
||||||
|
self.download_stories(userids=list(profiles), fast_update=fast_update, filename_target=None,
|
||||||
|
storyitem_filter=storyitem_filter)
|
||||||
|
|
||||||
def download_profile(self, profile_name: Union[str, Profile],
|
def download_profile(self, profile_name: Union[str, Profile],
|
||||||
profile_pic: bool = True, profile_pic_only: bool = False,
|
profile_pic: bool = True, profile_pic_only: bool = False,
|
||||||
fast_update: bool = False,
|
fast_update: bool = False,
|
||||||
download_stories: bool = False, download_stories_only: bool = False,
|
download_stories: bool = False, download_stories_only: bool = False,
|
||||||
|
download_tagged: bool = False, download_tagged_only: bool = False,
|
||||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
storyitem_filter: Optional[Callable[[StoryItem], bool]] = None) -> None:
|
storyitem_filter: Optional[Callable[[StoryItem], bool]] = None) -> None:
|
||||||
"""Download one profile"""
|
"""Download one profile
|
||||||
|
|
||||||
|
.. deprecated:: 4.1
|
||||||
|
Use :meth:`Instaloader.download_profiles`.
|
||||||
|
"""
|
||||||
|
|
||||||
# Get profile main page json
|
# Get profile main page json
|
||||||
# check if profile does exist or name has changed since last download
|
# check if profile does exist or name has changed since last download
|
||||||
@ -739,6 +923,13 @@ class Instaloader:
|
|||||||
if download_stories_only:
|
if download_stories_only:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Download tagged, if requested
|
||||||
|
if download_tagged or download_tagged_only:
|
||||||
|
with self.context.error_catcher('Download tagged of {}'.format(profile_name)):
|
||||||
|
self.download_tagged(profile, fast_update=fast_update, post_filter=post_filter)
|
||||||
|
if download_tagged_only:
|
||||||
|
return
|
||||||
|
|
||||||
# Iterate over pictures and download them
|
# Iterate over pictures and download them
|
||||||
self.context.log("Retrieving posts from profile {}.".format(profile_name))
|
self.context.log("Retrieving posts from profile {}.".format(profile_name))
|
||||||
totalcount = profile.mediacount
|
totalcount = profile.mediacount
|
||||||
|
@ -57,7 +57,7 @@ class InstaloaderContext:
|
|||||||
self.quiet = quiet
|
self.quiet = quiet
|
||||||
self.max_connection_attempts = max_connection_attempts
|
self.max_connection_attempts = max_connection_attempts
|
||||||
self._graphql_page_length = 50
|
self._graphql_page_length = 50
|
||||||
self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 20
|
self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 200
|
||||||
self._root_rhx_gis = None
|
self._root_rhx_gis = None
|
||||||
|
|
||||||
# error log, filled with error() and printed at the end of Instaloader.main()
|
# error log, filled with error() and printed at the end of Instaloader.main()
|
||||||
@ -69,6 +69,9 @@ class InstaloaderContext:
|
|||||||
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
|
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
|
||||||
self.raise_all_errors = False
|
self.raise_all_errors = False
|
||||||
|
|
||||||
|
# Cache profile from id (mapping from id to Profile)
|
||||||
|
self.profile_id_cache = dict()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def anonymous_copy(self):
|
def anonymous_copy(self):
|
||||||
session = self._session
|
session = self._session
|
||||||
@ -218,7 +221,7 @@ class InstaloaderContext:
|
|||||||
def _sleep(self):
|
def _sleep(self):
|
||||||
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
|
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
|
||||||
if self.sleep:
|
if self.sleep:
|
||||||
time.sleep(min(random.expovariate(0.6), 5.0))
|
time.sleep(min(random.expovariate(0.7), 5.0))
|
||||||
|
|
||||||
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
|
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
|
||||||
session: Optional[requests.Session] = None, _attempt=1) -> Dict[str, Any]:
|
session: Optional[requests.Session] = None, _attempt=1) -> Dict[str, Any]:
|
||||||
|
@ -402,6 +402,8 @@ class Profile:
|
|||||||
:param profile_id: userid
|
:param profile_id: userid
|
||||||
:raises: :class:`ProfileNotExistsException`, :class:`ProfileHasNoPicsException`
|
:raises: :class:`ProfileNotExistsException`, :class:`ProfileHasNoPicsException`
|
||||||
"""
|
"""
|
||||||
|
if profile_id in context.profile_id_cache:
|
||||||
|
return context.profile_id_cache[profile_id]
|
||||||
data = context.graphql_query("472f257a40c653c64c666ce877d59d2b",
|
data = context.graphql_query("472f257a40c653c64c666ce877d59d2b",
|
||||||
{'id': str(profile_id), 'first': 1},
|
{'id': str(profile_id), 'first': 1},
|
||||||
rhx_gis=context.root_rhx_gis)['data']['user']
|
rhx_gis=context.root_rhx_gis)['data']['user']
|
||||||
@ -415,7 +417,9 @@ class Profile:
|
|||||||
raise ProfileHasNoPicsException("Profile with ID {0}: no pics found.".format(str(profile_id)))
|
raise ProfileHasNoPicsException("Profile with ID {0}: no pics found.".format(str(profile_id)))
|
||||||
else:
|
else:
|
||||||
raise LoginRequiredException("Login required to determine username (ID: " + str(profile_id) + ").")
|
raise LoginRequiredException("Login required to determine username (ID: " + str(profile_id) + ").")
|
||||||
return Post(context, data['edges'][0]['node']).owner_profile
|
profile = Post(context, data['edges'][0]['node']).owner_profile
|
||||||
|
context.profile_id_cache[profile_id] = profile
|
||||||
|
return profile
|
||||||
|
|
||||||
def _asdict(self):
|
def _asdict(self):
|
||||||
json_node = self._node.copy()
|
json_node = self._node.copy()
|
||||||
@ -574,7 +578,9 @@ class Profile:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def profile_pic_url(self) -> str:
|
def profile_pic_url(self) -> str:
|
||||||
"""Return URL of profile picture"""
|
"""Return URL of profile picture
|
||||||
|
|
||||||
|
.. versionadded:: 4.0.3"""
|
||||||
try:
|
try:
|
||||||
return self._iphone_struct['hd_profile_pic_url_info']['url']
|
return self._iphone_struct['hd_profile_pic_url_info']['url']
|
||||||
except (InstaloaderException, KeyError) as err:
|
except (InstaloaderException, KeyError) as err:
|
||||||
@ -614,7 +620,9 @@ class Profile:
|
|||||||
self._metadata('edge_saved_media')))
|
self._metadata('edge_saved_media')))
|
||||||
|
|
||||||
def get_tagged_posts(self) -> Iterator[Post]:
|
def get_tagged_posts(self) -> Iterator[Post]:
|
||||||
"""Retrieve all posts where a profile is tagged."""
|
"""Retrieve all posts where a profile is tagged.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0.7"""
|
||||||
self._obtain_metadata()
|
self._obtain_metadata()
|
||||||
yield from (Post(self._context, node, self if int(node['owner']['id']) == self.userid else None) for node in
|
yield from (Post(self._context, node, self if int(node['owner']['id']) == self.userid else None) for node in
|
||||||
self._context.graphql_node_list("e31a871f7301132ceaab56507a66bbb7",
|
self._context.graphql_node_list("e31a871f7301132ceaab56507a66bbb7",
|
||||||
@ -780,7 +788,7 @@ class Story:
|
|||||||
# story is a Story object
|
# story is a Story object
|
||||||
for item in story.get_items():
|
for item in story.get_items():
|
||||||
# item is a StoryItem object
|
# item is a StoryItem object
|
||||||
L.download_storyitem(item, ':stores')
|
L.download_storyitem(item, ':stories')
|
||||||
|
|
||||||
This class implements == and is hashable.
|
This class implements == and is hashable.
|
||||||
|
|
||||||
@ -803,7 +811,7 @@ class Story:
|
|||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(self._unique_id)
|
return hash(self.unique_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
@ -866,6 +874,83 @@ class Story:
|
|||||||
yield from (StoryItem(self._context, item, self.owner_profile) for item in reversed(self._node['items']))
|
yield from (StoryItem(self._context, item, self.owner_profile) for item in reversed(self._node['items']))
|
||||||
|
|
||||||
|
|
||||||
|
class Highlight(Story):
|
||||||
|
"""
|
||||||
|
Structure representing a user's highlight with its associated story items.
|
||||||
|
|
||||||
|
Provides methods for accessing highlight properties, as well as :meth:`Highlight.get_items` to request associated
|
||||||
|
:class:`StoryItem` nodes. Highlights are returned by :meth:`Instaloader.get_highlights`.
|
||||||
|
|
||||||
|
With a logged-in :class:`Instaloader` instance `L`, you may download all highlights of a :class:`Profile` instance
|
||||||
|
USER with::
|
||||||
|
|
||||||
|
for highlight in L.get_highlights(USER):
|
||||||
|
# highlight is a Highlight object
|
||||||
|
for item in highlight.get_items():
|
||||||
|
# item is a StoryItem object
|
||||||
|
L.download_storyitem(item, '{}/{}'.format(highlight.owner_username, highlight.title))
|
||||||
|
|
||||||
|
This class implements == and is hashable.
|
||||||
|
|
||||||
|
:param context: :class:`InstaloaderContext` instance used for additional queries if necessary.
|
||||||
|
:param node: Dictionary containing the available information of the highlight as returned by Instagram.
|
||||||
|
:param owner: :class:`Profile` instance representing the owner profile of the highlight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, context: InstaloaderContext, node: Dict[str, Any], owner: Optional[Profile] = None):
|
||||||
|
super().__init__(context, node)
|
||||||
|
self._owner_profile = owner
|
||||||
|
self._items = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Highlight by {}: {}>'.format(self.owner_username, self.title)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> int:
|
||||||
|
"""A unique ID identifying this set of highlights."""
|
||||||
|
return int(self._node['id'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner_profile(self) -> Profile:
|
||||||
|
""":class:`Profile` instance of the highlights' owner."""
|
||||||
|
if not self._owner_profile:
|
||||||
|
self._owner_profile = Profile(self._context, self._node['owner'])
|
||||||
|
return self._owner_profile
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self) -> str:
|
||||||
|
"""The title of these highlights."""
|
||||||
|
return self._node['title']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cover_url(self) -> str:
|
||||||
|
"""URL of the highlights' cover."""
|
||||||
|
return self._node['cover_media']['thumbnail_src']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cover_cropped_url(self) -> str:
|
||||||
|
"""URL of the cropped version of the cover."""
|
||||||
|
return self._node['cover_media_cropped_thumbnail']['url']
|
||||||
|
|
||||||
|
def _fetch_items(self):
|
||||||
|
if not self._items:
|
||||||
|
self._items = self._context.graphql_query("45246d3fe16ccc6577e0bd297a5db1ab",
|
||||||
|
{"reel_ids": [], "tag_names": [], "location_ids": [],
|
||||||
|
"highlight_reel_ids": [str(self.unique_id)],
|
||||||
|
"precomposed_overlay": False})['data']['reels_media'][0]['items']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def itemcount(self) -> int:
|
||||||
|
"""Count of items associated with the :class:`Highlight` instance."""
|
||||||
|
self._fetch_items()
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
def get_items(self) -> Iterator[StoryItem]:
|
||||||
|
"""Retrieve all associated highlight items."""
|
||||||
|
self._fetch_items()
|
||||||
|
yield from (StoryItem(self._context, item, self.owner_profile) for item in self._items)
|
||||||
|
|
||||||
|
|
||||||
JsonExportable = Union[Post, Profile, StoryItem]
|
JsonExportable = Union[Post, Profile, StoryItem]
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from itertools import islice
|
|||||||
|
|
||||||
import instaloader
|
import instaloader
|
||||||
|
|
||||||
|
PROFILE_WITH_HIGHLIGHTS = 325732271
|
||||||
PUBLIC_PROFILE = "selenagomez"
|
PUBLIC_PROFILE = "selenagomez"
|
||||||
PUBLIC_PROFILE_ID = 460563723
|
PUBLIC_PROFILE_ID = 460563723
|
||||||
HASHTAG = "kitten"
|
HASHTAG = "kitten"
|
||||||
@ -102,6 +103,14 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
|||||||
for item in user_story.get_items():
|
for item in user_story.get_items():
|
||||||
print(item)
|
print(item)
|
||||||
|
|
||||||
|
def test_highlights_paging(self):
|
||||||
|
for user_highlight in self.L.get_highlights(PROFILE_WITH_HIGHLIGHTS):
|
||||||
|
print("Retrieving {} highlights \"{}\" from profile {}".format(user_highlight.itemcount,
|
||||||
|
user_highlight.title,
|
||||||
|
user_highlight.owner_username))
|
||||||
|
for item in user_highlight.get_items():
|
||||||
|
print(item)
|
||||||
|
|
||||||
def test_private_profile_paging(self):
|
def test_private_profile_paging(self):
|
||||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user