diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index c30c8db..04f3fbc 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -89,6 +89,7 @@ Instaloader supports the following targets: Posts tagged with a given location; the location ID is the numerical ID Instagram labels a location with (e.g. \https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/). + Requires :option:`--login`. .. versionadded:: 4.2 @@ -122,7 +123,7 @@ downloads the pictures and videos and their captions. You can specify - :option:`--geotags` **download geotags** of each post and save them as - Google Maps link, + Google Maps link (requires :option:`--login`), For a reference of all supported command line options, see :ref:`command-line-options`. diff --git a/docs/cli-options.rst b/docs/cli-options.rst index 5cb5c80..731d309 100644 --- a/docs/cli-options.rst +++ b/docs/cli-options.rst @@ -61,8 +61,7 @@ What to Download of each Post **Download geotags** when available. Geotags are stored as a text file with the location's name and a Google Maps link. This requires an additional - request to the Instagram server for each picture, which is why it is disabled - by default. + request to the Instagram server for each picture. Requires :option:`--login`. .. option:: --comments, -C diff --git a/instaloader/__main__.py b/instaloader/__main__.py index 7573ee5..cf51d53 100644 --- a/instaloader/__main__.py +++ b/instaloader/__main__.py @@ -98,6 +98,9 @@ def _main(instaloader: Instaloader, targetlist: List[str], else: instaloader.interactive_login(username) instaloader.context.log("Logged in as %s." % username) + # since 4.2.9 login is required for geotags + if instaloader.download_geotags and not instaloader.context.is_logged_in: + instaloader.context.error("Warning: Use --login to download geotags of posts.") # Try block for KeyboardInterrupt (save session on ^C) profiles = set() anonymous_retry_profiles = set() @@ -223,7 +226,8 @@ def main(): help="Download all followees of profile. Requires --login. " "Consider using :feed rather than @yourself.") g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.") - g_targets.add_argument('_location', nargs='*', metavar='%location_id', help="Download %%location_id.") + g_targets.add_argument('_location', nargs='*', metavar='%location_id', + help="Download %%location_id. Requires --login.") g_targets.add_argument('_feed', nargs='*', metavar=":feed", help="Download pictures from your feed. Requires --login.") g_targets.add_argument('_stories', nargs='*', metavar=":stories", @@ -258,7 +262,7 @@ def main(): help='Download geotags when available. Geotags are stored as a ' 'text file with the location\'s name and a Google Maps link. ' 'This requires an additional request to the Instagram ' - 'server for each picture, which is why it is disabled by default.') + 'server for each picture. Requires --login.') g_post.add_argument('-C', '--comments', action='store_true', help='Download and update comments for each post. ' 'This requires an additional request to the Instagram ' diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 79fbe02..bffa3cf 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -47,8 +47,6 @@ def _requires_login(func: Callable) -> Callable: if not instaloader.context.is_logged_in: raise LoginRequiredException("--login=USERNAME required.") return func(instaloader, *args, **kwargs) - docstring_text = ":raises LoginRequiredException: If called without being logged in.\n" - call.__doc__ = call.__doc__ + docstring_text if call.__doc__ is not None else docstring_text return call @@ -386,6 +384,7 @@ class Instaloader: """Saves internally stored :class:`requests.Session` object. :param filename: Filename, or None to use default filename. + :raises LoginRequiredException: If called without being logged in. """ if filename is None: assert self.context.username is not None @@ -510,6 +509,7 @@ class Instaloader: To use this, one needs to be logged in :param userids: List of user IDs to be processed in terms of downloading their stories, or None. + :raises LoginRequiredException: If called without being logged in. """ if not userids: @@ -546,6 +546,7 @@ class Instaloader: :param filename_target: Replacement for {target} in dirname_pattern and filename_pattern or None if profile name should be used instead :param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded + :raises LoginRequiredException: If called without being logged in. """ if not userids: @@ -605,6 +606,7 @@ class Instaloader: .. versionadded:: 4.1 :param user: ID or Profile of the user whose highlights should get fetched. + :raises LoginRequiredException: If called without being logged in. """ userid = user if isinstance(user, int) else user.userid @@ -634,6 +636,7 @@ class Instaloader: :param filename_target: Replacement for {target} in dirname_pattern and filename_pattern or None if profile name and the highlights' titles should be used instead :param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded + :raises LoginRequiredException: If called without being logged in. """ for user_highlight in self.get_highlights(user): name = user_highlight.owner_username @@ -659,6 +662,7 @@ class Instaloader: """Get Posts of the user's feed. :return: Iterator over Posts of the user's feed. + :raises LoginRequiredException: If called without being logged in. """ data = self.context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"] @@ -692,6 +696,7 @@ class Instaloader: :param max_count: Maximum count of pictures to download :param fast_update: If true, abort when first already-downloaded picture is encountered :param post_filter: function(post), which returns True if given picture should be downloaded + :raises LoginRequiredException: If called without being logged in. """ self.context.log("Retrieving pictures from your feed...") count = 1 @@ -717,6 +722,7 @@ class Instaloader: :param max_count: Maximum count of pictures to download :param fast_update: If true, abort when first already-downloaded picture is encountered :param post_filter: function(post), which returns True if given picture should be downloaded + :raises LoginRequiredException: If called without being logged in. """ self.context.log("Retrieving saved posts...") count = 1 @@ -734,12 +740,17 @@ class Instaloader: if fast_update and not downloaded: break + @_requires_login def get_location_posts(self, location: str) -> Iterator[Post]: """Get Posts which are listed by Instagram for a given Location. :return: Iterator over Posts of a location's posts + :raises LoginRequiredException: If called without being logged in. .. versionadded:: 4.2 + + .. versionchanged:: 4.2.9 + Require being logged in (as required by Instagram) """ has_next_page = True end_cursor = None @@ -754,6 +765,7 @@ class Instaloader: has_next_page = location_data['page_info']['has_next_page'] end_cursor = location_data['page_info']['end_cursor'] + @_requires_login def download_location(self, location: str, max_count: Optional[int] = None, post_filter: Optional[Callable[[Post], bool]] = None, @@ -769,8 +781,12 @@ class Instaloader: :param max_count: Maximum count of pictures to download :param post_filter: function(post), which returns True if given picture should be downloaded :param fast_update: If true, abort when first already-downloaded picture is encountered + :raises LoginRequiredException: If called without being logged in. .. versionadded:: 4.2 + + .. versionchanged:: 4.2.9 + Require being logged in (as required by Instagram) """ self.context.log("Retrieving pictures for location {}...".format(location)) count = 1 @@ -792,6 +808,7 @@ class Instaloader: """Get Posts which are worthy of exploring suggested by Instagram. :return: Iterator over Posts of the user's suggested posts. + :raises LoginRequiredException: If called without being logged in. """ data = self.context.get_json('explore/', {}) yield from (Post(self.context, node) diff --git a/instaloader/structures.py b/instaloader/structures.py index 92427bd..b638d9d 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -408,10 +408,17 @@ class Post: @property def location(self) -> Optional[PostLocation]: - """If the Post has a location, returns PostLocation namedtuple with fields 'id', 'lat' and 'lng' and 'name'.""" + """ + If the Post has a location, returns PostLocation namedtuple with fields 'id', 'lat' and 'lng' and 'name'. + + .. versionchanged:: 4.2.9 + Require being logged in (as required by Instagram), return None if not logged-in. + """ loc = self._field("location") if self._location or not loc: return self._location + if not self._context.is_logged_in: + return None location_id = int(loc['id']) if any(k not in loc for k in ('name', 'slug', 'has_public_page', 'lat', 'lng')): loc = self._context.get_json("explore/locations/{0}/".format(location_id), diff --git a/test/instaloader_unittests.py b/test/instaloader_unittests.py index e822dab..91115da 100644 --- a/test/instaloader_unittests.py +++ b/test/instaloader_unittests.py @@ -77,15 +77,6 @@ class TestInstaloaderAnonymously(unittest.TestCase): if count == PAGING_MAX_COUNT: break - def test_location_download(self): - self.L.download_location(LOCATION, NORMAL_MAX_COUNT) - - def test_location_paging(self): - for count, post in enumerate(self.L.get_location_posts(LOCATION)): - print(post) - if count == PAGING_MAX_COUNT: - break - def test_get_id_by_username(self): self.assertEqual(PUBLIC_PROFILE_ID, instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid) @@ -193,6 +184,15 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously): if count == PAGING_MAX_COUNT: break + def test_location_download(self): + self.L.download_location(LOCATION, NORMAL_MAX_COUNT) + + def test_location_paging(self): + for count, post in enumerate(self.L.get_location_posts(LOCATION)): + print(post) + if count == PAGING_MAX_COUNT: + break + if __name__ == '__main__': unittest.main()