From 3211d63ec1dd93213609b37697ef0d28ae2c7c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Koch-Kramer?= Date: Sun, 13 May 2018 19:43:49 +0200 Subject: [PATCH] Replace deleted field has_highlight_reel Since the property has_highlight_reel is no longer available throught the previously used graphql query, this information needs to be obtained in another way. Therefore the properties has_highlight_reels, has_public_story and has_viewable_story were added to the Profile class. Since has_public_story can be obtained throught graphql queries without being rate limited when invoked anonymously, the ability to use an anonymous copy of the context was added to to the InstaloaderContext class. Fixes #116 --- instaloader/instaloader.py | 2 +- instaloader/instaloadercontext.py | 17 +++++++++++-- instaloader/structures.py | 40 +++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) 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: