From 26e74bad6a4769025148d9b783e9f3980bc9ac0e Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 17 Jan 2019 16:36:59 +0100 Subject: [PATCH] Download HD profile picture if logged in Unfortunately, it is now required to be logged in to access the HD version of profile pictures. When attempting to download profile pictures without --login, a warning message is printed once, and the lower-quality versions are obtained. For backwards compatibility, already-downloaded profile pictures are overwritten if the now-obtainable version is assumed to be of better quality than the existing one (determined by file size vs Content-Length). The iPhone endpoint is accessed with code exhumed from c355338010a2a94c8a75f3001d0ea879b4f42383. Also, this reverts "Profile: don't access removed iphone info endpoint" 08327c41172e125a003b7bb6a015b04c4645ef27. This fixes #209. --- instaloader/__main__.py | 2 ++ instaloader/instaloader.py | 4 +++- instaloader/instaloadercontext.py | 18 ++++++++++++++++++ instaloader/structures.py | 31 ++++++++++++++++++++++++++++--- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/instaloader/__main__.py b/instaloader/__main__.py index 9cd2a49..007131d 100644 --- a/instaloader/__main__.py +++ b/instaloader/__main__.py @@ -179,6 +179,8 @@ def _main(instaloader: Instaloader, targetlist: List[str], if len(profiles) > 1: instaloader.context.log("Downloading {} profiles: {}".format(len(profiles), ' '.join([p.username for p in profiles]))) + if profiles and download_profile_pic and not instaloader.context.is_logged_in: + instaloader.context.error("Warning: Use --login to download HD version of profile pictures.") instaloader.download_profiles(profiles, download_profile_pic, download_posts, download_tagged, download_highlights, download_stories, fast_update, post_filter, storyitem_filter) diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 2a3e22a..99db1b0 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -309,7 +309,9 @@ class Instaloader: else: filename = '{0}/{1}_{2}_profile_pic.{3}'.format(self.dirname_pattern.format(), profile.username.lower(), profile_pic_identifier, profile_pic_extension) - if os.path.isfile(filename): + content_length = profile_pic_response.headers.get('Content-Length', None) + if os.path.isfile(filename) and (not self.context.is_logged_in or + content_length is not None and os.path.getsize(filename) >= int(content_length)): self.context.log(filename + ' already exists') return None self.context.write_raw(profile_pic_bytes if profile_pic_bytes else profile_pic_response, filename) diff --git a/instaloader/instaloadercontext.py b/instaloader/instaloadercontext.py index fb7b42f..0793bc9 100644 --- a/instaloader/instaloadercontext.py +++ b/instaloader/instaloadercontext.py @@ -429,6 +429,24 @@ class InstaloaderContext: data = _query() yield from (edge['node'] for edge in data['edges']) + def get_iphone_json(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]: + """JSON request to ``i.instagram.com``. + + :param path: URL, relative to ``i.instagram.com/`` + :param params: GET parameters + :return: Decoded response dictionary + :raises QueryReturnedBadRequestException: When the server responds with a 400. + :raises QueryReturnedNotFoundException: When the server responds with a 404. + :raises ConnectionException: When query repeatedly failed. + + .. versionadded:: 4.2.1""" + tempsession = copy_session(self._session) + tempsession.headers['User-Agent'] = 'Instagram 10.3.2 (iPhone7,2; iPhone OS 9_3_3; en_US; en-US; ' \ + 'scale=2.00; 750x1334) AppleWebKit/420+' + for header in ['Host', 'Origin', 'X-Instagram-AJAX', 'X-Requested-With']: + tempsession.headers.pop(header, None) + return self.get_json(path, params, 'i.instagram.com', tempsession) + def write_raw(self, resp: Union[bytes, requests.Response], filename: str) -> None: """Write raw response data into a file.""" self.log(filename, end=' ', flush=True) diff --git a/instaloader/structures.py b/instaloader/structures.py index 0bb5984..678d350 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -373,6 +373,10 @@ class Profile: self._has_public_story = None self._node = node self._rhx_gis = None + self._iphone_struct_ = None + if 'iphone_struct' in node: + # if loaded from JSON with load_structure_from_file() + self._iphone_struct_ = node['iphone_struct'] @classmethod def from_username(cls, context: InstaloaderContext, username: str): @@ -423,6 +427,8 @@ class Profile: json_node.pop('edge_media_collections', None) json_node.pop('edge_owner_to_timeline_media', None) json_node.pop('edge_saved_media', None) + if self._iphone_struct_: + json_node['iphone_struct'] = self._iphone_struct_ return json_node def _obtain_metadata(self): @@ -447,6 +453,15 @@ class Profile: d = d[key] return d + @property + def _iphone_struct(self) -> Dict[str, Any]: + if not self._context.is_logged_in: + raise LoginRequiredException("--login required to access iPhone profile info endpoint.") + if not self._iphone_struct_: + data = self._context.get_iphone_json(path='api/v1/users/{}/info/'.format(self.userid), params={}) + self._iphone_struct_ = data['user'] + return self._iphone_struct_ + @property def userid(self) -> int: """User ID""" @@ -563,10 +578,20 @@ class Profile: @property def profile_pic_url(self) -> str: - """Return URL of profile picture + """Return URL of profile picture. If logged in, the HD version is returned, otherwise a lower-quality version. - .. versionadded:: 4.0.3""" - return self._metadata("profile_pic_url_hd") + .. versionadded:: 4.0.3 + + .. versionchanged:: 4.2.1 + Require being logged in for HD version (as required by Instagram).""" + if self._context.is_logged_in: + try: + return self._iphone_struct['hd_profile_pic_url_info']['url'] + except (InstaloaderException, KeyError) as err: + self._context.error('{} Unable to fetch high quality profile pic.'.format(err)) + return self._metadata("profile_pic_url_hd") + else: + return self._metadata("profile_pic_url_hd") def get_profile_pic_url(self) -> str: """.. deprecated:: 4.0.3