diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 6a668fb..f0a0a21 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -701,7 +701,7 @@ class Instaloader: # Download stories, if requested if download_stories or download_stories_only: - if profile.has_highlight_reel: + if profile.has_viewable_story: with self.context.error_catcher("Download stories of {}".format(profile_name)): self.download_stories(userids=[profile.userid], filename_target=profile_name, fast_update=fast_update, storyitem_filter=storyitem_filter) diff --git a/instaloader/instaloadercontext.py b/instaloader/instaloadercontext.py index 8611eff..decaa5e 100644 --- a/instaloader/instaloadercontext.py +++ b/instaloader/instaloadercontext.py @@ -69,6 +69,16 @@ class InstaloaderContext: # Can be set to True for testing, disables supression of InstaloaderContext._error_catcher self.raise_all_errors = False + @contextmanager + def anonymous_copy(self): + session = self._session + username = self.username + self._session = self.get_anonymous_session() + self.username = None + yield self + self.username = username + self._session = session + @property def is_logged_in(self) -> bool: """True, if this Instaloader instance is logged in.""" @@ -212,7 +222,10 @@ class InstaloaderContext: return 0 return round(min(self.query_timestamps) + sliding_window - current_time) + 6 is_graphql_query = 'query_hash' in params and 'graphql/query' in path - if is_graphql_query: + # some queries are not rate limited if invoked anonymously: + query_not_limited = is_graphql_query and not self.is_logged_in \ + and params['query_hash'] in ['9ca88e465c3f866a76f7adee3871bdd8'] + if is_graphql_query and not query_not_limited: waittime = graphql_query_waittime() if waittime > 0: self.log('\nToo many queries in the last time. Need to wait {} seconds.'.format(waittime)) @@ -228,7 +241,7 @@ class InstaloaderContext: while resp.is_redirect: redirect_url = resp.headers['location'] self.log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url)) - if redirect_url.index('https://{}/'.format(host)) == 0: + if redirect_url.startswith('https://{}/'.format(host)): resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/', params=params, allow_redirects=False) else: diff --git a/instaloader/structures.py b/instaloader/structures.py index 137305b..4ae6592 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -358,6 +358,8 @@ class Profile: def __init__(self, context: InstaloaderContext, node: Dict[str, Any]): assert 'username' in node self._context = context + self._has_highlight_reels = None + self._has_public_story = None self._node = node self._rhx_gis = None @@ -484,8 +486,42 @@ class Profile: return self._metadata('has_blocked_viewer') @property - def has_highlight_reel(self) -> bool: - return self._metadata('has_highlight_reel') + def has_highlight_reels(self) -> bool: + """ + This becomes `True` if the :class:`Profile` has any stories currently available, + even if not viewable by the viewer. + """ + if not self._has_highlight_reels: + with self._context.anonymous_copy() as anonymous_context: + data = anonymous_context.get_json(path='api/v1/users/{}/info/'.format(self.userid), + params={}, host='i.instagram.com') + self._has_highlight_reels = data['user']['has_highlight_reels'] + return self._has_highlight_reels + + @property + def has_public_story(self) -> bool: + if not self._has_public_story: + self._obtain_metadata() + # query not rate limited if invoked anonymously: + with self._context.anonymous_copy() as anonymous_context: + data = anonymous_context.graphql_query('9ca88e465c3f866a76f7adee3871bdd8', + {'user_id': self.userid, 'include_chaining': False, + 'include_reel': False, 'include_suggested_users': False, + 'include_logged_out_extras': True, + 'include_highlight_reels': False}, + 'https://www.instagram.com/{}/'.format(self.username), + self._rhx_gis) + self._has_public_story = data['data']['user']['has_public_story'] + return self._has_public_story + + @property + def has_viewable_story(self) -> bool: + """ + Some stories are private. This property determines if the :class:`Profile` + has at least one story which can be viewed using the associated :class:`InstaloaderContext`, + i.e. the viewer has privileges to view it. + """ + return self.has_public_story or self.followed_by_viewer and self.has_highlight_reels @property def has_requested_viewer(self) -> bool: