diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index a3ad545..fae9bd4 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -85,6 +85,11 @@ Instaloader supports the following targets: - ``"#hashtag"`` Posts with a certain **hashtag** (the quotes are usually necessary), +- ``%location id`` + 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/). + - ``:stories`` The currently-visible **stories** of your followees (requires :option:`--login`), @@ -126,7 +131,7 @@ Filename Specification ^^^^^^^^^^^^^^^^^^^^^^ For each target, Instaloader creates a directory named after the target, -i.e. ``profile``, ``#hashtag``, ``:feed``, etc. and therein saves the +i.e. ``profile``, ``#hashtag``, ``%location id``, ``:feed``, etc. and therein saves the posts in files named after the post's timestamp. :option:`--dirname-pattern` allows to configure the directory name of each diff --git a/docs/cli-options.rst b/docs/cli-options.rst index 1d4f310..8489be7 100644 --- a/docs/cli-options.rst +++ b/docs/cli-options.rst @@ -8,7 +8,7 @@ Instaloader is invoked with:: $ instaloader [options] target [target ...] where ``target`` is a ``profile``, a ``"#hashtag"``, ``@profile`` (all profiles -that *profile* is following), or if logged in ``:feed`` (pictures from your +that *profile* is following), ``%location ID``, or if logged in ``:feed`` (pictures from your feed), ``:stories`` (stories of your followees) or ``:saved`` (collection of posts marked as saved). @@ -171,7 +171,7 @@ Which Posts to Download .. option:: --count COUNT, -c Do not attempt to download more than COUNT posts. Applies only to - ``#hashtag`` and ``:feed``. + ``#hashtag``, ``%location id``, and ``:feed``. Login (Download Private Profiles) diff --git a/docs/index.rst b/docs/index.rst index ce2d0ad..b68a691 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,7 +43,7 @@ See :ref:`install` for more options on how to install Instaloader. instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged] [--login YOUR-USERNAME] [--fast-update] - profile | "#hashtag" | + profile | "#hashtag" | %location_id | :stories | :feed | :saved See :ref:`download-pictures-from-instagram` for a detailed introduction on how diff --git a/instaloader/__main__.py b/instaloader/__main__.py index 9caf8bd..a387fe9 100644 --- a/instaloader/__main__.py +++ b/instaloader/__main__.py @@ -20,7 +20,7 @@ def usage_string(): return """ {0} [--comments] [--geotags] [--stories] [--highlights] [--tagged] {2:{1}} [--login YOUR-USERNAME] [--fast-update] -{2:{1}} profile | "#hashtag" | :stories | :feed | :saved +{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved {0} --help""".format(argv0, len(argv0), '') @@ -130,6 +130,9 @@ def _main(instaloader: Instaloader, targetlist: List[str], post_filter=post_filter) elif target[0] == '-': instaloader.download_post(Post.from_shortcode(instaloader.context, target[1:]), target) + elif target[0] == "%": + instaloader.download_location(location=target[1:], max_count=max_count, fast_update=fast_update, + post_filter=post_filter) elif target == ":feed": instaloader.download_feed_posts(fast_update=fast_update, max_count=max_count, post_filter=post_filter) @@ -208,6 +211,7 @@ 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('_feed', nargs='*', metavar=":feed", help="Download pictures from your feed. Requires --login.") g_targets.add_argument('_stories', nargs='*', metavar=":stories", diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index 024d822..681546a 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -647,6 +647,55 @@ class Instaloader: if fast_update and not downloaded: break + 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 + """ + has_next_page = True + end_cursor = None + while has_next_page: + if end_cursor: + params = {'__a': 1, 'max_id': end_cursor} + else: + params = {'__a': 1} + location_data = self.context.get_json('explore/locations/{0}/'.format(location), + params)['graphql']['location']['edge_location_to_media'] + yield from (Post(self.context, edge['node']) for edge in location_data['edges']) + has_next_page = location_data['page_info']['has_next_page'] + end_cursor = location_data['page_info']['end_cursor'] + + def download_location(self, location: str, + max_count: Optional[int] = None, + post_filter: Optional[Callable[[Post], bool]] = None, + fast_update: bool = False) -> None: + """Download pictures of one location. + + To download the last 30 pictures with location 362629379, do:: + + loader = Instaloader() + loader.download_location(362629379, max_count=30) + + :param location: Location to download, as Instagram numerical ID + :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 + """ + self.context.log("Retrieving pictures for location {}...".format(location)) + count = 1 + for post in self.get_location_posts(location): + if max_count is not None and count > max_count: + break + self.context.log('[{0:3d}] %{1} '.format(count, location), end='', flush=True) + if post_filter is not None and not post_filter(post): + self.context.log('') + continue + count += 1 + with self.context.error_catcher('Download location {}'.format(location)): + downloaded = self.download_post(post, target='%' + location) + if fast_update and not downloaded: + break + @_requires_login def get_explore_posts(self) -> Iterator[Post]: """Get Posts which are worthy of exploring suggested by Instagram. diff --git a/test/instaloader_unittests.py b/test/instaloader_unittests.py index 7e80e64..a2fd6be 100644 --- a/test/instaloader_unittests.py +++ b/test/instaloader_unittests.py @@ -12,6 +12,7 @@ PROFILE_WITH_HIGHLIGHTS = 325732271 PUBLIC_PROFILE = "selenagomez" PUBLIC_PROFILE_ID = 460563723 HASHTAG = "kitten" +LOCATION = "362629379" OWN_USERNAME = "aandergr" NORMAL_MAX_COUNT = 2 PAGING_MAX_COUNT = 15 @@ -69,6 +70,15 @@ 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)