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