From 13612c606e5b702f03eaee8c1983be45be231e0c Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sun, 23 Aug 2020 12:10:53 +0200 Subject: [PATCH 01/21] Use more reliable query for own profile for :saved Adds a method Profile.own_profile() which uses the test_login() query for obtaining the own profile. This method is used for accesssing saved posts (Profile.get_saved_posts() on own profile). Also removes the now-unneeded self._obtain_metadata() call in Profile.get_saved_posts(), and changes the NodeIterator such that data is never None and the count property is more likely available. Fixes #563. --- instaloader/instaloader.py | 2 +- instaloader/nodeiterator.py | 16 ++++++++++------ instaloader/structures.py | 13 +++++++++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 6d08c8b..773b51a 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -856,7 +856,7 @@ class Instaloader: """ self.context.log("Retrieving saved posts...") assert self.context.username is not None # safe due to @_requires_login; required by typechecker - node_iterator = Profile.from_username(self.context, self.context.username).get_saved_posts() + node_iterator = Profile.own_profile(self.context).get_saved_posts() self.posts_download_loop(node_iterator, ":saved", fast_update, post_filter, max_count=max_count, total_count=node_iterator.count) diff --git a/instaloader/nodeiterator.py b/instaloader/nodeiterator.py index c74b88c..c72fe46 100644 --- a/instaloader/nodeiterator.py +++ b/instaloader/nodeiterator.py @@ -81,11 +81,13 @@ class NodeIterator(Iterator[T]): self._node_wrapper = node_wrapper self._query_variables = query_variables if query_variables is not None else {} self._query_referer = query_referer - self._data = first_data self._page_index = 0 self._total_index = 0 - self._best_before = (None if first_data is None else - datetime.now() + NodeIterator._shelf_life) + if first_data is not None: + self._data = first_data + self._best_before = datetime.now() + NodeIterator._shelf_life + else: + self._data = self._query() def _query(self, after: Optional[str] = None) -> Dict: pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any] @@ -113,8 +115,6 @@ class NodeIterator(Iterator[T]): return self def __next__(self) -> T: - if self._data is None: - self._data = self._query() if self._page_index < len(self._data['edges']): node = self._data['edges'][self._page_index]['node'] page_index, total_index = self._page_index, self._total_index @@ -193,8 +193,12 @@ class NodeIterator(Iterator[T]): self._query_referer != frozen.query_referer or self._context.username != frozen.context_username): raise InvalidArgumentException("Mismatching resume information.") + if not frozen.best_before: + raise InvalidArgumentException("\"best before\" date missing.") + if frozen.remaining_data is None: + raise InvalidArgumentException("\"remaining_data\" missing.") self._total_index = frozen.total_index - self._best_before = datetime.fromtimestamp(frozen.best_before) if frozen.best_before else None + self._best_before = datetime.fromtimestamp(frozen.best_before) self._data = frozen.remaining_data diff --git a/instaloader/structures.py b/instaloader/structures.py index 6c53071..430aebb 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -579,6 +579,17 @@ class Profile: context.profile_id_cache[profile_id] = profile return profile + @classmethod + def own_profile(cls, context: InstaloaderContext): + """Return own profile if logged-in. + + :param context: :attr:`Instaloader.context` + + .. versionadded:: 4.5.2""" + if not context.is_logged_in: + raise LoginRequiredException("--login required to access own profile.") + return cls(context, context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]["user"]) + def _asdict(self): json_node = self._node.copy() # remove posts to avoid "Circular reference detected" exception @@ -802,7 +813,6 @@ class Profile: if self.username != self._context.username: raise LoginRequiredException("--login={} required to get that profile's saved posts.".format(self.username)) - self._obtain_metadata() return NodeIterator( self._context, 'f883d95537fbcd400f466f63d42bd8a1', @@ -810,7 +820,6 @@ class Profile: lambda n: Post(self._context, n), {'id': self.userid}, 'https://www.instagram.com/{0}/'.format(self.username), - self._metadata('edge_saved_media'), ) def get_tagged_posts(self) -> NodeIterator[Post]: From f53bdf9ef823dac5a0f91489e3c4b622c8126ade Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sun, 23 Aug 2020 15:56:15 +0200 Subject: [PATCH 02/21] Do not call get_sidecar_nodes() if not needed Do not call Post.get_sidecar_nodes() in download_post() if both --no-pictures and --no-videos are set. --- instaloader/instaloader.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 773b51a..bf63a8c 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -514,15 +514,16 @@ class Instaloader: # Download the image(s) / video thumbnail and videos within sidecars if desired downloaded = True if post.typename == 'GraphSidecar': - for edge_number, sidecar_node in enumerate(post.get_sidecar_nodes(), start=1): - if self.download_pictures and (not sidecar_node.is_video or self.download_video_thumbnails): - # Download sidecar picture or video thumbnail (--no-pictures implies --no-video-thumbnails) - downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url, - mtime=post.date_local, filename_suffix=str(edge_number)) - if sidecar_node.is_video and self.download_videos: - # Download sidecar video if desired - downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url, - mtime=post.date_local, filename_suffix=str(edge_number)) + if self.download_pictures or self.download_videos: + for edge_number, sidecar_node in enumerate(post.get_sidecar_nodes(), start=1): + if self.download_pictures and (not sidecar_node.is_video or self.download_video_thumbnails): + # Download sidecar picture or video thumbnail (--no-pictures implies --no-video-thumbnails) + downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url, + mtime=post.date_local, filename_suffix=str(edge_number)) + if sidecar_node.is_video and self.download_videos: + # Download sidecar video if desired + downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url, + mtime=post.date_local, filename_suffix=str(edge_number)) elif post.typename == 'GraphImage': # Download picture if self.download_pictures: From a7a05c200c2f9ac3cb97e5ee599196268e4e913d Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sun, 23 Aug 2020 15:58:38 +0200 Subject: [PATCH 03/21] Adjust RateController to current observations Fixes #774. --- instaloader/instaloadercontext.py | 83 +++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/instaloader/instaloadercontext.py b/instaloader/instaloadercontext.py index bf2e0ef..5695f4d 100644 --- a/instaloader/instaloadercontext.py +++ b/instaloader/instaloadercontext.py @@ -549,8 +549,8 @@ class RateController: def __init__(self, context: InstaloaderContext): self._context = context - self._graphql_query_timestamps = dict() # type: Dict[str, List[float]] - self._graphql_earliest_next_request_time = 0.0 + self._query_timestamps = dict() # type: Dict[str, List[float]] + self._earliest_next_request_time = 0.0 def sleep(self, secs: float): """Wait given number of seconds.""" @@ -560,11 +560,11 @@ class RateController: time.sleep(secs) def _dump_query_timestamps(self, current_time: float, failed_query_type: str): - windows = [10, 11, 15, 20, 30, 60] + windows = [10, 11, 20, 22, 30, 60] self._context.error("Requests within last {} minutes grouped by type:" .format('/'.join(str(w) for w in windows)), repeat_at_end=False) - for query_type, times in self._graphql_query_timestamps.items(): + for query_type, times in self._query_timestamps.items(): reqs_in_sliding_window = [sum(t > current_time - w * 60 for t in times) for w in windows] self._context.error(" {} {:>32}: {}".format( "*" if query_type == failed_query_type else " ", @@ -573,28 +573,61 @@ class RateController: ), repeat_at_end=False) def count_per_sliding_window(self, query_type: str) -> int: - """Return how many GraphQL requests can be done within the sliding window.""" + """Return how many requests can be done within the sliding window.""" # Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as # whether we are logged in. - # pylint:disable=no-self-use,unused-argument - return 200 + # pylint:disable=no-self-use + return 100 if query_type in ['iphone', 'other'] else 200 + + def _reqs_in_sliding_window(self, query_type: Optional[str], current_time: float, window: float) -> List[float]: + if query_type is not None: + # timestamps of type query_type + relevant_timestamps = self._query_timestamps[query_type] + else: + # all GraphQL queries, i.e. not 'iphone' or 'other' + graphql_query_timestamps = filter(lambda tp: tp[0] not in ['iphone', 'other'], + self._query_timestamps.items()) + relevant_timestamps = [t for times in (tp[1] for tp in graphql_query_timestamps) for t in times] + return list(filter(lambda t: t > current_time - window, relevant_timestamps)) def query_waittime(self, query_type: str, current_time: float, untracked_queries: bool = False) -> float: - """Calculate time needed to wait before GraphQL query can be executed.""" - sliding_window = 660 - if query_type not in self._graphql_query_timestamps: - self._graphql_query_timestamps[query_type] = [] - self._graphql_query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60, - self._graphql_query_timestamps[query_type])) - reqs_in_sliding_window = list(filter(lambda t: t > current_time - sliding_window, - self._graphql_query_timestamps[query_type])) - count_per_sliding_window = self.count_per_sliding_window(query_type) - if len(reqs_in_sliding_window) < count_per_sliding_window and not untracked_queries: - return max(0.0, self._graphql_earliest_next_request_time - current_time) - next_request_time = min(reqs_in_sliding_window) + sliding_window + 6 - if untracked_queries: - self._graphql_earliest_next_request_time = next_request_time - return max(next_request_time, self._graphql_earliest_next_request_time) - current_time + """Calculate time needed to wait before query can be executed.""" + per_type_sliding_window = 660 + if query_type not in self._query_timestamps: + self._query_timestamps[query_type] = [] + self._query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60, + self._query_timestamps[query_type])) + + def per_type_next_request_time(): + reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window) + if len(reqs_in_sliding_window) < self.count_per_sliding_window(query_type): + return 0.0 + else: + return min(reqs_in_sliding_window) + per_type_sliding_window + 6 + + def gql_accumulated_next_request_time(): + if query_type in ['iphone', 'other']: + return 0.0 + gql_accumulated_sliding_window = 600 + gql_accumulated_max_count = 275 + reqs_in_sliding_window = self._reqs_in_sliding_window(None, current_time, gql_accumulated_sliding_window) + if len(reqs_in_sliding_window) < gql_accumulated_max_count: + return 0.0 + else: + return min(reqs_in_sliding_window) + gql_accumulated_sliding_window + + def untracked_next_request_time(): + if untracked_queries: + reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window) + self._earliest_next_request_time = min(reqs_in_sliding_window) + per_type_sliding_window + 6 + return self._earliest_next_request_time + + return max(0.0, + max( + per_type_next_request_time(), + gql_accumulated_next_request_time(), + untracked_next_request_time(), + ) - current_time) def wait_before_query(self, query_type: str) -> None: """This method is called before a query to Instagram. It calls :meth:`RateController.sleep` to wait @@ -606,10 +639,10 @@ class RateController: .format(round(waittime), datetime.now() + timedelta(seconds=waittime))) if waittime > 0: self.sleep(waittime) - if query_type not in self._graphql_query_timestamps: - self._graphql_query_timestamps[query_type] = [time.monotonic()] + if query_type not in self._query_timestamps: + self._query_timestamps[query_type] = [time.monotonic()] else: - self._graphql_query_timestamps[query_type].append(time.monotonic()) + self._query_timestamps[query_type].append(time.monotonic()) def handle_429(self, query_type: str) -> None: """This method is called to handle a 429 Too Many Requests response. It calls :meth:`RateController.sleep` to From 438669b9541711dda627c240c5f5199e0ec4e6cd Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sun, 23 Aug 2020 18:55:22 +0200 Subject: [PATCH 04/21] Release of Version 4.5.2 --- instaloader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instaloader/__init__.py b/instaloader/__init__.py index 626be3b..e96065a 100644 --- a/instaloader/__init__.py +++ b/instaloader/__init__.py @@ -1,7 +1,7 @@ """Download pictures (or videos) along with their captions and other metadata from Instagram.""" -__version__ = '4.5.1' +__version__ = '4.5.2' try: From 6d3c73eec368b6d1cef283d0a8241039b335eaf1 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Mon, 31 Aug 2020 20:29:21 +0200 Subject: [PATCH 05/21] Adjust RateController to current observations --- instaloader/instaloadercontext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instaloader/instaloadercontext.py b/instaloader/instaloadercontext.py index 5695f4d..ffca86e 100644 --- a/instaloader/instaloadercontext.py +++ b/instaloader/instaloadercontext.py @@ -577,7 +577,7 @@ class RateController: # Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as # whether we are logged in. # pylint:disable=no-self-use - return 100 if query_type in ['iphone', 'other'] else 200 + return 75 if query_type in ['iphone', 'other'] else 200 def _reqs_in_sliding_window(self, query_type: Optional[str], current_time: float, window: float) -> List[float]: if query_type is not None: From 574cadbb8271bd8ba3c39db9d26bec183d2f2db8 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Mon, 31 Aug 2020 20:35:13 +0200 Subject: [PATCH 06/21] Adjust bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- docs/contributing.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fa8acc4..e2aa9a0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -13,7 +13,7 @@ Steps to reproduce the behavior: (e.g. Instaloader command line) **Expected behavior** -A clear and concise description of what you expected to happen (if not obvious). +A clear and concise description of what you expected to happen (even if it seems obvious). **Error messages and tracebacks** If applicable, add error messages and tracebacks to help explain your problem. diff --git a/docs/contributing.rst b/docs/contributing.rst index 21850a0..f27b70a 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -32,7 +32,7 @@ reporting a problem, please keep the following in mind: - Include all **error messages and tracebacks** in the report. -- If not obvious, describe **which behavior you expected** +- Even if it seems obvious, describe **which behavior you expected** instead of what actually happened. - If we have closed an issue apparently inadvertently or inappropriately, please From 886530d66134056ba566ed1248f189419290b7a5 Mon Sep 17 00:00:00 2001 From: naohide <57.x.mas@gmail.com> Date: Fri, 4 Sep 2020 03:20:02 +0900 Subject: [PATCH 07/21] Fix wrong ProfileNotExistsException in some edge cases (#785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes wrong "Profile does not exist" error. Co-authored-by: André Koch-Kramer Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com> --- instaloader/instaloadercontext.py | 4 ++-- instaloader/structures.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/instaloader/instaloadercontext.py b/instaloader/instaloadercontext.py index ffca86e..a138092 100644 --- a/instaloader/instaloadercontext.py +++ b/instaloader/instaloadercontext.py @@ -351,10 +351,10 @@ class InstaloaderContext: raise ConnectionException("\"window._sharedData\" does not contain required keys.") # If GraphQL data is missing in `window._sharedData`, search for it in `__additionalDataLoaded`. if 'graphql' not in post_or_profile_page[0]: - match = re.search(r'window\.__additionalDataLoaded\([^{]+{"graphql":({.*})}\);', + match = re.search(r'window\.__additionalDataLoaded\(.*?({.*"graphql":.*})\);', resp.text) if match is not None: - post_or_profile_page[0]['graphql'] = json.loads(match.group(1)) + post_or_profile_page[0]['graphql'] = json.loads(match.group(1))['graphql'] return resp_json else: resp_json = resp.json() diff --git a/instaloader/structures.py b/instaloader/structures.py index 430aebb..78219a6 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -549,7 +549,7 @@ class Profile: """ # pylint:disable=protected-access profile = cls(context, {'username': username.lower()}) - profile._obtain_metadata() # to raise ProfileNotExistException now in case username is invalid + profile._obtain_metadata() # to raise ProfileNotExistsException now in case username is invalid return profile @classmethod From 4ad4584b0d636b463fb4af2ee127f6eb55ab39bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Koch-Kramer?= Date: Thu, 3 Sep 2020 20:23:57 +0200 Subject: [PATCH 08/21] Release of Version 4.5.3 --- instaloader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instaloader/__init__.py b/instaloader/__init__.py index e96065a..8783240 100644 --- a/instaloader/__init__.py +++ b/instaloader/__init__.py @@ -1,7 +1,7 @@ """Download pictures (or videos) along with their captions and other metadata from Instagram.""" -__version__ = '4.5.2' +__version__ = '4.5.3' try: From c53625028d3860ffbbd48d4f1d17ba997e8d3b5f Mon Sep 17 00:00:00 2001 From: Ingo Marquardt Date: Fri, 18 Sep 2020 11:23:06 +0200 Subject: [PATCH 09/21] Avoid additional http request to get post's owner ID. (#809) --- instaloader/structures.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/instaloader/structures.py b/instaloader/structures.py index 78219a6..5fb7d3b 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -207,7 +207,13 @@ class Post: @property def owner_id(self) -> int: """The ID of the Post's owner.""" - return self.owner_profile.userid + # The ID may already be available, e.g. if the post instance was created + # from an `hashtag.get_posts()` iterator, so no need to make another + # http request. + if 'owner' in self._node and 'id' in self._node['owner']: + return self._node['owner']['id'] + else: + return self.owner_profile.userid @property def date_local(self) -> datetime: From 0586a1fec3c427eeb190d7fd4cd84519a56a73f3 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 19 Sep 2020 17:07:57 +0200 Subject: [PATCH 10/21] Update docs/troubleshooting.rst --- docs/troubleshooting.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 861e5ca..7fa8fbf 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -9,10 +9,7 @@ Troubleshooting --------------------- Instaloader has a logic to keep track of its requests to Instagram and to obey -their rate limits. Since they are nowhere documented, we try them out -experimentally. We have a daily cron job running to confirm that Instaloader -still stays within the rate limits. Nevertheless, the rate control logic assumes -that +their rate limits. The rate controller assumes that - at one time, Instaloader is the only application that consumes requests, i.e. neither the Instagram browser interface, nor a mobile app, nor another @@ -21,7 +18,13 @@ that - no requests had been consumed when Instaloader starts. The latter one implies that restarting or reinstantiating Instaloader often -within short time is prone to cause a 429. If a request is denied with a 429, +within short time is prone to cause a 429. + +Since the behavior of the rate controller might change between different +versions of Instaloader, make sure to use the current version of Instaloader, +especially when encountering many 429 errors. + +If a request is denied with a 429, Instaloader retries the request as soon as the temporary ban is assumed to be expired. In case the retry continuously fails for some reason, which should not happen under normal conditions, consider adjusting the @@ -37,8 +40,7 @@ Too many queries in the last time **"Too many queries in the last time"** is not an error. It is a notice that the rate limit has almost been reached, according to Instaloader's own rate -accounting mechanism. We regularly adjust this mechanism to match Instagram's -current rate limiting. +accounting mechanism. Private but not followed ------------------------ @@ -57,9 +59,8 @@ pointing the user to an URL to be opened in a browser. Nevertheless, in :issue:`92` and :issue:`615` users reported problems with logging in. We recommend to always keep the session file which Instaloader creates when using :option:`--login`. If a session file is present, -:option:`--login` does not make make use of the failure-prone login procedure. -Also, session files usually do not expire and can be copied between different -computers without any problems. +:option:`--login` does not make use of the failure-prone login procedure. +Also, session files usually do not expire. If you do not have a session file present, you may use the following script (:example:`615_import_firefox_session.py`) to workaround login problems by From f4ecfea64cc11efba44cda2b44c8cfe41adbd28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Koch-Kramer?= Date: Mon, 28 Sep 2020 21:14:38 +0200 Subject: [PATCH 11/21] Add Android to docs installation page --- docs/installation.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index d27f268..5a16523 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -33,6 +33,9 @@ To **upgrade Instaloader** to its current version, do:: - On **Windows 10**, you may download the standalone executable from the `current release page `__. +- On **Android**, you can use Instaloader with `Termux `__ + after typing ``pkg install python`` and ``pip3 install instaloader``. + - To test the most current **pre-release** version of Instaloader:: pip3 install --pre instaloader From 74f7970b50ffdeacebed434f0f643c338039224b Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:20:31 +0100 Subject: [PATCH 12/21] Fix a KeyError when fetching post metadata fails Fixes #824. --- instaloader/structures.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/instaloader/structures.py b/instaloader/structures.py index 5fb7d3b..86c00a6 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -147,10 +147,6 @@ class Post: ) self._full_metadata_dict = pic_json['data']['shortcode_media'] if self._full_metadata_dict is None: - # issue #449 - self._context.error("Fetching Post metadata failed (issue #449). " - "The following data has been returned:\n" - + json.dumps(pic_json['entry_data'], indent=2)) raise BadResponseException("Fetching Post metadata failed.") if self.shortcode != self._full_metadata_dict['shortcode']: self._node.update(self._full_metadata_dict) From c7372e36cc488d65df76f177b2e4946719cdacf9 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 14 Nov 2020 18:57:49 +0100 Subject: [PATCH 13/21] Require being logged in for Post.get_likes() Closes #860. --- instaloader/structures.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/instaloader/structures.py b/instaloader/structures.py index 86c00a6..f41bf05 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -437,7 +437,14 @@ class Post: ) def get_likes(self) -> Iterator['Profile']: - """Iterate over all likes of the post. A :class:`Profile` instance of each likee is yielded.""" + """ + Iterate over all likes of the post. A :class:`Profile` instance of each likee is yielded. + + .. versionchanged:: 4.5.4 + Require being logged in (as required by Instagram). + """ + if not self._context.is_logged_in: + raise LoginRequiredException("--login required to access likes of a post.") if self.likes == 0: # Avoid doing additional requests if there are no comments return From cc2986cff3d9212682b8db333acf6bbae265b467 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 14 Nov 2020 19:53:43 +0100 Subject: [PATCH 14/21] Fix Hashtag.get_all_posts() missing one post in certain cases When a hashtag query returned only the 9 top posts, get_all_posts() yielded only 8 instead of all 9 posts. Closes #853. --- instaloader/structures.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/instaloader/structures.py b/instaloader/structures.py index f41bf05..3dd31ef 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -1359,9 +1359,13 @@ class Hashtag: next_other = next(other_posts, None) while next_top is not None or next_other is not None: if next_other is None: + assert next_top is not None + yield next_top yield from sorted_top_posts break if next_top is None: + assert next_other is not None + yield next_other yield from other_posts break if next_top == next_other: From e70faf506dfec440c0442e0fcda2f07df7263c91 Mon Sep 17 00:00:00 2001 From: Rafael Ristovski Date: Thu, 19 Nov 2020 11:02:32 +0100 Subject: [PATCH 15/21] Update graphql query hash used for stories (#872) This updated graph query hash returns all fields for `tappable_objects`. These fields were missing/partial when using the old query hash. This allows to get the full json metadata of a story. --- instaloader/instaloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index bf63a8c..8d53231 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -584,7 +584,7 @@ class Instaloader: yield userids[i:i + userids_per_query] for userid_chunk in _userid_chunks(): - stories = self.context.graphql_query("bf41e22b1c4ba4c9f31b844ebb7d9056", + stories = self.context.graphql_query("303a4ae99711322310f25250d988f3b7", {"reel_ids": userid_chunk, "precomposed_overlay": False})["data"] yield from (Story(self.context, media) for media in stories['reels_media']) From 03ac067e71565df02cb242687a7144e3a1ab9fd3 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:08:31 +0100 Subject: [PATCH 16/21] Add Contributor Covenant Code of Conduct --- CODE_OF_CONDUCT.md | 135 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..61c1dbd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,135 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement by opening an +issue or contacting one or more of the project maintainers. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + From 6ad03d3575113ae6db3f695c61aff0f1d6ca90b0 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:11:45 +0100 Subject: [PATCH 17/21] Link Insights -> Contributors in AUTHORS.md --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index bd1661d..8dd917f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -6,3 +6,4 @@ Instaloader is written by - Alexander Graf (@aandergr) - André Koch-Kramer (@Thammus) - Lars Lindqvist (@e5150) +- ... and many more, see https://github.com/instaloader/instaloader/graphs/contributors From 6824d7b75c82bd5692f2b006c77a2568f27fd235 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:13:10 +0100 Subject: [PATCH 18/21] Update year in LICENSE file --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index ded9107..e620fd1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016-2019 Alexander Graf and André Koch-Kramer. +Copyright (c) 2016-2020 Alexander Graf and André Koch-Kramer. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 1c577716dd600400e8b95cd9e83f64908691e351 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:15:51 +0100 Subject: [PATCH 19/21] Fix HTTP error 500 when accessing :stories in certain cases Based on a comment in #795: https://github.com/instaloader/instaloader/issues/795#issuecomment-726634493 Closes #795. --- instaloader/instaloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 8d53231..6022d3e 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -579,7 +579,7 @@ class Instaloader: def _userid_chunks(): assert userids is not None - userids_per_query = 100 + userids_per_query = 50 for i in range(0, len(userids), userids_per_query): yield userids[i:i + userids_per_query] From 6d90c47d9a00e4d69d80e599f62a11e64e85f8aa Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:29:18 +0100 Subject: [PATCH 20/21] Troubleshooting: Mention overriding RateController --- docs/troubleshooting.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 7fa8fbf..35ca45b 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -35,6 +35,9 @@ promiscuous IP addresses, such as cloud, VPN and public proxy services, might be subject to significantly stricter limits for anonymous access. However, logged-in accesses (see :option:`--login`) do not seem to be affected. +Instaloader allows to adjust the rate controlling behavior by overriding +:class:`instaloader.RateController`. + Too many queries in the last time --------------------------------- @@ -42,6 +45,9 @@ Too many queries in the last time rate limit has almost been reached, according to Instaloader's own rate accounting mechanism. +Instaloader allows to adjust the rate controlling behavior by overriding +:class:`instaloader.RateController`. + Private but not followed ------------------------ From e9648bc551cc19725fbbe1000435229209797a28 Mon Sep 17 00:00:00 2001 From: Alexander Graf <17130992+aandergr@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:31:05 +0100 Subject: [PATCH 21/21] Release of Version 4.5.4 --- instaloader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instaloader/__init__.py b/instaloader/__init__.py index 8783240..6d4e90a 100644 --- a/instaloader/__init__.py +++ b/instaloader/__init__.py @@ -1,7 +1,7 @@ """Download pictures (or videos) along with their captions and other metadata from Instagram.""" -__version__ = '4.5.3' +__version__ = '4.5.4' try: