diff --git a/docs/as-module.rst b/docs/as-module.rst index 82e2cd3..aa3a7e0 100644 --- a/docs/as-module.rst +++ b/docs/as-module.rst @@ -155,6 +155,14 @@ Profiles .. autoclass:: Profile :no-show-inheritance: +TopSearchResults +"""""""""""""""" + +.. autoclass:: TopSearchResults + :no-show-inheritance: + + .. versionadded:: 4.3 + Loading and Saving """""""""""""""""" diff --git a/instaloader/__init__.py b/instaloader/__init__.py index 326731d..f80d4b4 100644 --- a/instaloader/__init__.py +++ b/instaloader/__init__.py @@ -16,4 +16,4 @@ from .exceptions import * from .instaloader import Instaloader from .instaloadercontext import InstaloaderContext from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation, Profile, Story, - StoryItem, load_structure_from_file, save_structure_to_file) + StoryItem, TopSearchResults, load_structure_from_file, save_structure_to_file) diff --git a/instaloader/structures.py b/instaloader/structures.py index f97aa5e..041780d 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -1060,6 +1060,73 @@ class Highlight(Story): yield from (StoryItem(self._context, item, self.owner_profile) for item in self._items) +class TopSearchResults: + """ + An invocation of this class triggers a search on Instagram for the provided search string. + + Provides methods to access the search results as profiles (:class:`Profile`), locations (:class:`PostLocation`) and + hashtags. + + :param context: :attr:`Instaloader.context` used to send the query for the search. + :param searchstring: String to search for with Instagram's "top search". + """ + + def __init__(self, context: InstaloaderContext, searchstring: str): + self._context = context + self._searchstring = searchstring + # The `__a` param is only needed to prevent `get_json()` from searching for 'window._sharedData'. + self._node = context.get_json('web/search/topsearch/', + params={'context': 'blended', + 'query': searchstring, + 'include_reel': False, + '__a': 1}) + + def get_profiles(self) -> Iterator[Profile]: + """ + Provides the :class:`Profile` instances from the search result. + """ + for user in self._node.get('users', []): + user_node = user['user'] + if 'pk' in user_node: + user_node['id'] = user_node['pk'] + yield Profile(self._context, user_node) + + def get_prefixed_usernames(self) -> Iterator[str]: + """ + Provides all profile names from the search result that start with the search string. + """ + for user in self._node.get('users', []): + username = user.get('user', {}).get('username', '') + if username.startswith(self._searchstring): + yield username + + def get_locations(self) -> Iterator[PostLocation]: + """ + Provides instances of :class:`PostLocation` from the search result. + """ + for location in self._node.get('places', []): + place = location.get('place', {}) + slug = place.get('slug') + loc = place.get('location', {}) + yield PostLocation(int(loc['pk']), loc['name'], slug, None, loc['lat'], loc['lng']) + + def get_hashtag_strings(self) -> Iterator[str]: + """ + Provides the hashtags from the search result as strings. + """ + for hashtag in self._node.get('hashtags', []): + name = hashtag.get('hashtag', {}).get('name') + if name: + yield name + + @property + def searchstring(self) -> str: + """ + The string that was searched for on Instagram to produce this :class:`TopSearchResults` instance. + """ + return self._searchstring + + JsonExportable = Union[Post, Profile, StoryItem]