Compare commits

..
18 Commits
Author SHA1 Message Date
Alexander Graf dcea0e9672 Release of version 4.0.7 2018-08-05 09:37:18 +02:00
Alexander Graf 809b23a996 doc: Mark Profile.has_higlight_reels as deprecated 2018-08-05 09:13:49 +02:00
Alexander Graf c634f21dcc Fix anon-retry for profiles that blocked viewer
This was broken with 50a5330fec.
2018-08-05 09:01:22 +02:00
Alexander Graf a070ed42ae get_stories(): Fetch in chunks of 100 users
It seems there is a limit of how many user stories can be requested at once.
Now, Instaloader.get_stories() chunks the stories request.

Fixes #157 and fixes #134.
2018-08-05 08:06:53 +02:00
Alexander Graf a34d750152 rate handling: Show %H:%M when request is retried 2018-08-05 08:02:35 +02:00
Lars Lindqvist bfa5d8ed41 Fix :stories
50a5330fec breaks `:stories` if userids is None (non-iterable).
2018-08-05 03:29:24 +02:00
Alexander Graf 7f81985911 _ArbitraryItemFormatter: Allow chaining attributes
Override get_value() rather than get_field(), to allow chaining of item
attributes, such as --post-metadata-txt={owner_profile.profile_pic_url}.

Motivated by #151.
2018-08-04 11:41:19 +02:00
Lars Lindqvist b5aa268344 Fix owner_profile for Profile.get_tagged_posts() 2018-08-04 09:41:23 +02:00
André Koch-Kramer 92653dcdb9 Release of version 4.0.6 2018-08-02 16:55:59 +02:00
André Koch-Kramer 1b5c12c8fa Add unittest for Profile.get_tagged_posts() 2018-08-02 16:48:06 +02:00
Lars Lindqvist bc29ffbd44 Add meth:get_tagged_posts to Profile 2018-08-02 16:41:51 +02:00
André Koch-Kramer 50a5330fec Change download of stories when not using :stories
If using --stories or --stories-only the stories got donwloaded along
with the profiles one by one. Now, the stories get downloaded in a
similar aproach like when using the :stories target, i.e.
download_stories() gets only called once.
Profile.has_highlight_reels is broken and now always returns true.
This fixes #153.
2018-08-02 16:20:27 +02:00
André Koch-Kramer 1739844758 Fix login by allowing more http headers
Because Instagram is bombarding over hundret of headers on login it is
necessary to increase http.client._MAXHEADERS.
Fixes #152.
2018-07-30 17:50:22 +02:00
André Koch-Kramer 0ebb223214 Added BTC address to donations section
Motivated by #130.
2018-07-23 23:36:46 +02:00
Alexander Graf 3d4109ded3 travis disable pylint's no-else-return 2018-07-22 11:04:11 +02:00
Alexander Graf 8423e3bd00 Mention IRC channel #instaloader on Freenode 2018-07-22 10:55:29 +02:00
Alexander Graf 5b0f8b75cc Add AUTHORS.md listing Instaloader's authors 2018-07-22 10:54:54 +02:00
Alexander Graf 3cb33603e5 doc: Use Instaloader brand colors for links 2018-07-09 21:32:18 +02:00
12 changed files with 191 additions and 58 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ install:
- pip install pylint requests - pip install pylint requests
- pip install -r docs/requirements.txt - pip install -r docs/requirements.txt
script: script:
- python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements,inconsistent-return-statements,invalid-name,wildcard-import,unused-wildcard-import instaloader - python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements,inconsistent-return-statements,invalid-name,wildcard-import,unused-wildcard-import,no-else-return instaloader
- make -C docs html SPHINXOPTS="-W -n" - make -C docs html SPHINXOPTS="-W -n"
deploy: deploy:
- provider: pypi - provider: pypi
+8
View File
@@ -0,0 +1,8 @@
Authors
=======
Instaloader is written by
- Alexander Graf (@aandergr)
- André Koch-Kramer (@Thammus)
- Lars Lindqvist (@e5150)
+28 -1
View File
@@ -75,6 +75,33 @@ Disclaimer
Instaloader is in no way affiliated with, authorized, maintained or endorsed by Instagram or any of its affiliates or Instaloader is in no way affiliated with, authorized, maintained or endorsed by Instagram or any of its affiliates or
subsidiaries. This is an independent and unofficial project. Use at your own risk. subsidiaries. This is an independent and unofficial project. Use at your own risk.
Instaloader is licensed under an MIT license. Refer to ``LICENSE`` file for more information.
.. disclaimer-end .. disclaimer-end
Instaloader is licensed under an MIT license. Refer to ``LICENSE`` file for more information. Contributing
------------
As an open source project, Instaloader heavily depends on the contributions from
its community. See
`contributing <https://instaloader.github.io/contributing.html>`__
for how you may help Instaloader to become an even greater tool.
It is a pleasure for us to share our Instaloader to the world, and we are proud
to have attracted such an active and motivating community, with so many users
who share their suggestions and ideas with us. Buying a community-sponsored beer
or coffee from time to time is very likely to further raise our passion for the
development of Instaloader.
| For Donations, we provide a PayPal.Me link and a Bitcoin address.
| PayPal: `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
For a direct and personal communication within Instaloader's community, we have
an IRC channel
**#instaloader** on **Freenode**.
Freenode provides a `Web IRC Client
<https://webchat.freenode.net/?channels=instaloader>`__, allowing to join our
channel without having an IRC client installed.
+8
View File
@@ -10,3 +10,11 @@ dd {
dl.option dd { dl.option dd {
padding-left: 12px; padding-left: 12px;
} }
a {
color: #008d06;
}
a:hover {
color: #f48400;
}
+20 -3
View File
@@ -17,7 +17,7 @@ Reporting Bugs
If you encounter a bug, do not hesitate to report it in our If you encounter a bug, do not hesitate to report it in our
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. Since `Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. Since
Instaloader is actively developed, the majority of bugs is fixed within only Instaloader is actively developed, the majority of bugs is fixed within only
**4 days** after being reported. When reporting a problem, please keep the **3 days** after being reported. When reporting a problem, please keep the
following in mind: following in mind:
- Ensure you **use the latest version** of Instaloader. The currently-installed - Ensure you **use the latest version** of Instaloader. The currently-installed
@@ -95,9 +95,26 @@ who share their suggestions and ideas with us. Buying a community-sponsored beer
or coffee from time to time is very likely to further raise our passion for the or coffee from time to time is very likely to further raise our passion for the
development of Instaloader. development of Instaloader.
We have a PayPal.Me link for donations: `PayPal.me/aandergr | For donations, we provide a PayPal.Me link and a Bitcoin address.
<https://www.paypal.me/aandergr>`__ | `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
.. donations-end .. donations-end
.. (Discussion in :issue:`130`) .. (Discussion in :issue:`130`)
IRC Channel
-----------
.. ircchannel-start
For a direct and personal communication within Instaloader's community, we have
an IRC channel
**#instaloader** on **Freenode**.
Freenode provides a `Web IRC Client
<https://webchat.freenode.net/?channels=instaloader>`__, allowing to join our
channel without having an IRC client installed.
.. ircchannel-end
+4
View File
@@ -88,6 +88,10 @@ become an even greater tool.
:start-after: donations-start :start-after: donations-start
:end-before: donations-end :end-before: donations-end
.. include:: contributing.rst
:start-after: ircchannel-start
:end-before: ircchannel-end
.. ..
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
+1 -1
View File
@@ -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.5' __version__ = '4.0.7'
try: try:
+47 -21
View File
@@ -90,6 +90,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
instaloader.context.log("Logged in as %s." % username) instaloader.context.log("Logged in as %s." % username)
# Try block for KeyboardInterrupt (save session on ^C) # Try block for KeyboardInterrupt (save session on ^C)
profiles = set() profiles = set()
anonymous_retry_profiles = set()
try: try:
# Generate set of profiles, already downloading non-profile targets # Generate set of profiles, already downloading non-profile targets
for target in targetlist: for target in targetlist:
@@ -121,8 +122,9 @@ def _main(instaloader: Instaloader, targetlist: List[str],
if target[0] == '@': if target[0] == '@':
instaloader.context.log("Retrieving followees of %s..." % target[1:]) instaloader.context.log("Retrieving followees of %s..." % target[1:])
profile = Profile.from_username(instaloader.context, target[1:]) profile = Profile.from_username(instaloader.context, target[1:])
followees = profile.get_followees() for followee in profile.get_followees():
profiles.update([followee.username for followee in followees]) instaloader.save_profile_id(followee)
profiles.add(followee)
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)
@@ -135,26 +137,50 @@ def _main(instaloader: Instaloader, targetlist: List[str],
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count, instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
post_filter=post_filter) post_filter=post_filter)
else: else:
profiles.add(target) try:
profile = instaloader.check_profile_id(target)
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
if not stories_only and not profile.is_private:
raise ProfileNotExistsException("{} blocked you; But she's public, "
"so we just download her anonymously.".format(target))
else:
instaloader.context.error("{} blocked you.".format(target))
else:
profiles.add(profile)
except ProfileNotExistsException as err:
# 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.
if instaloader.context.is_logged_in and not stories_only:
instaloader.context.log(err)
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
with instaloader.anonymous_copy() as anonymous_loader:
with instaloader.context.error_catcher():
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target))
instaloader.context.log("Looks good.")
else:
raise
if len(profiles) > 1: if len(profiles) > 1:
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles), ' '.join(profiles))) instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
# Iterate through profiles list and download them ' '.join([p.username for p in profiles])))
for target in profiles: if not stories_only:
with instaloader.context.error_catcher(target): # Iterate through profiles list and download them
try: for target in profiles:
instaloader.download_profile(target, profile_pic, profile_pic_only, fast_update, with instaloader.context.error_catcher(target):
stories, stories_only, post_filter=post_filter, instaloader.download_profile(target, profile_pic, profile_pic_only,
storyitem_filter=storyitem_filter) fast_update, post_filter=post_filter)
except ProfileNotExistsException as err: if anonymous_retry_profiles:
if instaloader.context.is_logged_in: instaloader.context.log("Downloading anonymously: {}"
instaloader.context.log(err) .format(' '.join([p.username for p in anonymous_retry_profiles])))
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: for target in anonymous_retry_profiles:
with instaloader.context.error_catcher(): with instaloader.context.error_catcher(target):
anonymous_loader.download_profile(target, profile_pic, profile_pic_only, anonymous_loader.download_profile(target, profile_pic, profile_pic_only,
fast_update, post_filter=post_filter) fast_update, post_filter=post_filter)
else: if stories or stories_only:
raise 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
+46 -26
View File
@@ -11,7 +11,7 @@ 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 Callable, Iterator, List, Optional, Any from typing import Any, Callable, Iterator, List, Optional, Union
from .exceptions import * from .exceptions import *
from .instaloadercontext import InstaloaderContext from .instaloadercontext import InstaloaderContext
@@ -49,11 +49,11 @@ class _ArbitraryItemFormatter(string.Formatter):
def __init__(self, item: Any): def __init__(self, item: Any):
self._item = item self._item = item
def get_field(self, field_name, args, kwargs): def get_value(self, key, args, kwargs):
"""Override to substitute {ATTRIBUTE} by attributes of our _item.""" """Override to substitute {ATTRIBUTE} by attributes of our _item."""
if hasattr(self._item, field_name): if hasattr(self._item, key):
return self._item.__getattribute__(field_name), None return getattr(self._item, key)
return super().get_field(field_name, args, kwargs) return super().get_value(key, args, kwargs)
def format_field(self, value, format_spec): def format_field(self, value, format_spec):
"""Override :meth:`string.Formatter.format_field` to have our """Override :meth:`string.Formatter.format_field` to have our
@@ -396,37 +396,45 @@ class Instaloader:
:param userids: List of user IDs to be processed in terms of downloading their stories, or None. :param userids: List of user IDs to be processed in terms of downloading their stories, or None.
""" """
if userids is None: if not userids:
data = self.context.graphql_query("d15efd8c0c5b23f0ef71f18bf363c704", data = self.context.graphql_query("d15efd8c0c5b23f0ef71f18bf363c704",
{"only_stories": True})["data"]["user"] {"only_stories": True})["data"]["user"]
if data is None: if data is None:
raise BadResponseException('Bad stories reel JSON.') raise BadResponseException('Bad stories reel JSON.')
userids = list(edge["node"]["id"] for edge in data["feed_reels_tray"]["edge_reels_tray_to_reel"]["edges"]) userids = list(edge["node"]["id"] for edge in data["feed_reels_tray"]["edge_reels_tray_to_reel"]["edges"])
stories = self.context.graphql_query("bf41e22b1c4ba4c9f31b844ebb7d9056", def _userid_chunks():
{"reel_ids": userids, "precomposed_overlay": False})["data"] userids_per_query = 100
for i in range(0, len(userids), userids_per_query):
yield userids[i:i + userids_per_query]
yield from (Story(self.context, media) for media in stories['reels_media']) for userid_chunk in _userid_chunks():
stories = self.context.graphql_query("bf41e22b1c4ba4c9f31b844ebb7d9056",
{"reel_ids": userid_chunk, "precomposed_overlay": False})["data"]
yield from (Story(self.context, media) for media in stories['reels_media'])
@_requires_login @_requires_login
def download_stories(self, def download_stories(self,
userids: Optional[List[int]] = None, userids: Optional[List[Union[int, Profile]]] = None,
fast_update: bool = False, fast_update: bool = False,
filename_target: str = ':stories', filename_target: Optional[str] = ':stories',
storyitem_filter: Optional[Callable[[StoryItem], bool]] = None) -> None: storyitem_filter: Optional[Callable[[StoryItem], bool]] = None) -> None:
""" """
Download available stories from user followees or all stories of users whose ID are given. Download available stories from user followees or all stories of users whose ID are given.
Does not mark stories as seen. Does not mark stories as seen.
To use this, one needs to be logged in To use this, one needs to be logged in
:param userids: List of user IDs to be processed in terms of downloading their stories :param userids: List of user IDs or Profiles to be processed in terms of downloading their stories
:param fast_update: If true, abort when first already-downloaded picture is encountered :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 :param filename_target: Replacement for {target} in dirname_pattern and filename_pattern
or None if profile name should be used instead
:param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded :param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded
""" """
if not userids: if not userids:
self.context.log("Retrieving all visible stories...") self.context.log("Retrieving all visible stories...")
else:
userids = [p if isinstance(p, int) else p.userid for p in userids]
for user_story in self.get_stories(userids): for user_story in self.get_stories(userids):
name = user_story.owner_username name = user_story.owner_username
@@ -440,7 +448,7 @@ class Instaloader:
self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True) self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
count += 1 count += 1
with self.context.error_catcher('Download story from user {}'.format(name)): with self.context.error_catcher('Download story from user {}'.format(name)):
downloaded = self.download_storyitem(item, filename_target) downloaded = self.download_storyitem(item, filename_target if filename_target else name)
if fast_update and not downloaded: if fast_update and not downloaded:
break break
@@ -611,6 +619,24 @@ class Instaloader:
if fast_update and not downloaded: if fast_update and not downloaded:
break break
def _get_id_filename(self, profile_name: str) -> str:
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
format_string_contains_key(self.dirname_pattern, 'target'))):
return '{0}/id'.format(self.dirname_pattern.format(profile=profile_name.lower(),
target=profile_name.lower()))
else:
return '{0}/{1}_id'.format(self.dirname_pattern.format(), profile_name.lower())
def save_profile_id(self, profile: Profile):
"""
Store ID of profile locally.
"""
os.makedirs(self.dirname_pattern.format(profile=profile.username,
target=profile.username), exist_ok=True)
with open(self._get_id_filename(profile.username), 'w') as text_file:
text_file.write(str(profile.userid) + "\n")
self.context.log("Stored ID {0} for profile {1}.".format(profile.userid, profile.username))
def check_profile_id(self, profile_name: str) -> Profile: def check_profile_id(self, profile_name: str) -> Profile:
""" """
Consult locally stored ID of profile with given name, check whether ID matches and whether name Consult locally stored ID of profile with given name, check whether ID matches and whether name
@@ -623,12 +649,7 @@ class Instaloader:
with suppress(ProfileNotExistsException): with suppress(ProfileNotExistsException):
profile = Profile.from_username(self.context, profile_name) profile = Profile.from_username(self.context, profile_name)
profile_exists = profile is not None profile_exists = profile is not None
if ((format_string_contains_key(self.dirname_pattern, 'profile') or id_filename = self._get_id_filename(profile_name)
format_string_contains_key(self.dirname_pattern, 'target'))):
id_filename = '{0}/id'.format(self.dirname_pattern.format(profile=profile_name.lower(),
target=profile_name.lower()))
else:
id_filename = '{0}/{1}_id'.format(self.dirname_pattern.format(), profile_name.lower())
try: try:
with open(id_filename, 'rb') as id_file: with open(id_filename, 'rb') as id_file:
profile_id = int(id_file.read()) profile_id = int(id_file.read())
@@ -657,15 +678,11 @@ class Instaloader:
except (FileNotFoundError, ValueError): except (FileNotFoundError, ValueError):
pass pass
if profile_exists: if profile_exists:
os.makedirs(self.dirname_pattern.format(profile=profile_name.lower(), self.save_profile_id(profile)
target=profile_name.lower()), exist_ok=True)
with open(id_filename, 'w') as text_file:
text_file.write(str(profile.userid) + "\n")
self.context.log("Stored ID {0} for profile {1}.".format(profile.userid, profile_name))
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_profile(self, profile_name: str, 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,
@@ -676,7 +693,10 @@ class Instaloader:
# 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
# and update name and json data if necessary # and update name and json data if necessary
profile = self.check_profile_id(profile_name.lower()) if isinstance(profile_name, str):
profile = self.check_profile_id(profile_name.lower())
else:
profile = profile_name
profile_name = profile.username profile_name = profile.username
+8 -2
View File
@@ -9,6 +9,7 @@ import textwrap
import time import time
import urllib.parse import urllib.parse
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta
from typing import Any, Callable, Dict, Iterator, Optional from typing import Any, Callable, Dict, Iterator, Optional
import requests import requests
@@ -175,6 +176,9 @@ class InstaloaderContext:
:raises InvalidArgumentException: If the provided username does not exist. :raises InvalidArgumentException: If the provided username does not exist.
:raises BadCredentialsException: If the provided password is wrong. :raises BadCredentialsException: If the provided password is wrong.
:raises ConnectionException: If connection to Instagram failed.""" :raises ConnectionException: If connection to Instagram failed."""
import http.client
# pylint:disable=protected-access
http.client._MAXHEADERS = 200
session = requests.Session() session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1', session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
'ig_vw': '1920', 'csrftoken': '', 'ig_vw': '1920', 'csrftoken': '',
@@ -243,7 +247,8 @@ class InstaloaderContext:
if is_graphql_query and not query_not_limited: if is_graphql_query and not query_not_limited:
waittime = graphql_query_waittime() waittime = graphql_query_waittime()
if waittime > 0: if waittime > 0:
self.log('\nToo many queries in the last time. Need to wait {} seconds.'.format(waittime)) self.log('\nToo many queries in the last time. Need to wait {} seconds, until {:%H:%M}.'
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
time.sleep(waittime) time.sleep(waittime)
if self.query_timestamps is not None: if self.query_timestamps is not None:
self.query_timestamps.append(time.monotonic()) self.query_timestamps.append(time.monotonic())
@@ -298,7 +303,8 @@ class InstaloaderContext:
if is_graphql_query: if is_graphql_query:
waittime = graphql_query_waittime(untracked_queries=True) waittime = graphql_query_waittime(untracked_queries=True)
if waittime > 0: if waittime > 0:
self.log('The request will be retried in {} seconds.'.format(waittime)) self.log('The request will be retried in {} seconds, at {:%H:%M}.'
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
time.sleep(waittime) time.sleep(waittime)
self._sleep() self._sleep()
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1) return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
+15 -3
View File
@@ -526,10 +526,12 @@ class Profile:
@property @property
def has_highlight_reels(self) -> bool: def has_highlight_reels(self) -> bool:
""" """
This becomes `True` if the :class:`Profile` has any stories currently available, .. deprecated:: 4.0.6
even if not viewable by the viewer. Always returns `True` since :issue:`153`.
Before broken, this indicated whether the :class:`Profile` had available stories.
""" """
return self._iphone_struct['has_highlight_reels'] return True
@property @property
def has_public_story(self) -> bool: def has_public_story(self) -> bool:
@@ -609,6 +611,16 @@ class Profile:
self._rhx_gis, self._rhx_gis,
self._metadata('edge_saved_media'))) self._metadata('edge_saved_media')))
def get_tagged_posts(self) -> Iterator[Post]:
"""Retrieve all posts where a profile is tagged."""
self._obtain_metadata()
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",
{'id': self.userid},
'https://www.instagram.com/{0}/'.format(self.username),
lambda d: d['data']['user']['edge_user_to_photos_of_you'],
self._rhx_gis))
def get_followers(self) -> Iterator['Profile']: def get_followers(self) -> Iterator['Profile']:
""" """
Retrieve list of followers of given profile. Retrieve list of followers of given profile.
+5
View File
@@ -76,6 +76,11 @@ class TestInstaloaderAnonymously(unittest.TestCase):
self.assertEqual(post, post2) self.assertEqual(post, post2)
break break
def test_public_profile_tagged_paging(self):
for post in islice(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_tagged_posts(),
PAGING_MAX_COUNT):
print(post)
class TestInstaloaderLoggedIn(TestInstaloaderAnonymously): class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):