From 8773e8efefecefb0788d2e9b91aa8235ebb26968 Mon Sep 17 00:00:00 2001 From: "Mikhail Andreev (adw0rd)" Date: Wed, 6 Jan 2021 23:01:47 +0300 Subject: [PATCH] Support stickers for story [#18] Added StoryLocation, change location to locations for upload story, fix story_sticker_ids --- README.md | 21 +++++++++++++---- instagrapi/extractors.py | 1 + instagrapi/mixins/photo.py | 48 ++++++++++++++++++++++++-------------- instagrapi/mixins/video.py | 47 ++++++++++++++++++++++++------------- instagrapi/types.py | 9 +++++++ setup.py | 2 +- tests.py | 35 +++++++++++++++++++++------ 7 files changed, 117 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index fe6106db..06dac86e 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ The current types are in [types.py](/instagrapi/types.py): | Comment | Comments to Media | | Story | Story | | StoryLink | Link (Swipe up) | +| StoryLocation | Tag Location in Story (as stocker) | | StoryMention | Mention users in Story (user, coordinates and dimensions) | | StoryHashtag | Hashtag for story (as sticker) | | StoryBuild | [StoryBuilder](/instagrapi/story.py) return path to photo/video and mention cordinats | @@ -356,7 +357,7 @@ Upload medias to your feed. Common arguments: * `path` - Path to source file * `caption` - Text for you post * `usertags` - List[Usertag] of mention users (see `Usertag` in [types.py](/instagrapi/types.py)) -* `location` - Location (e.g. `Location(lat=42.0, lng=42.0)`) +* `location` - Location (e.g. `Location(name='Test', lat=42.0, lng=42.0)`) | Method | Return | Description | | ------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------- | @@ -373,28 +374,38 @@ Upload medias to your stories. Common arguments: * `caption` - Caption for story (now use to fetch mentions) * `thumbnail` - Thumbnail instead capture from source file * `mentions` - Tag profiles in story -* `location` - Location (e.g. `Location(lat=42.0, lng=42.0)`) +* `locations` - Add locations to story * `links` - "Swipe Up" links (now use first) * `hashtags` - Add hashtags to story | Method | Return | Description | | ------------------------------------------------------------------------------------------------ | -------- | ------------- | -| photo_upload_to_story(path: Path, caption: str, upload_id: str, mentions: List[Usertag], location: Location, links: List[StoryLink], hashtags: List[StoryHashtag]) | Media | Upload photo (Support JPG files) -| video_upload_to_story(path: Path, caption: str, thumbnail: Path, mentions: List[Usertag], location: Location, links: List[StoryLink], hashtags: List[StoryHashtag]) | Media | Upload video (Support MP4 files) +| photo_upload_to_story(path: Path, caption: str, upload_id: str, mentions: List[Usertag], locations: List[StoryLocation], links: List[StoryLink], hashtags: List[StoryHashtag]) | Story | Upload photo (Support JPG files) +| video_upload_to_story(path: Path, caption: str, thumbnail: Path, mentions: List[Usertag], locations: List[StoryLocation], links: List[StoryLink], hashtags: List[StoryHashtag]) | Story | Upload video (Support MP4 files) Examples: ``` python +from instagrapi import Client +from instagrapi.types import Location, StoryMention, StoryLocation, StoryLink, StoryHashtag + +cl = Client() +cl.login(USERNAME, PASSWORD) + media_path = cl.video_download( cl.media_pk_from_url('https://www.instagram.com/p/CGgDsi7JQdS/') ) adw0rd = cl.user_info_by_username('adw0rd') +loc = cl.location_complete(Location(name='Test', lat=42.0, lng=42.0)) +ht = cl.hashtag_info('dhbastards') cl.video_upload_to_story( media_path, "Credits @adw0rd", mentions=[StoryMention(user=adw0rd, x=0.49892962, y=0.703125, width=0.8333333333333334, height=0.125)], - links=[StoryLink(webUri='https://github.com/adw0rd/instagrapi')] + locations=[StoryLocation(location=loc, x=0.33, y=0.22, width=0.4, height=0.7)], + links=[StoryLink(webUri='https://github.com/adw0rd/instagrapi')], + hashtags=[StoryHashtag(hashtag=ht, x=0.23, y=0.32, width=0.5, height=0.22)], ) ``` diff --git a/instagrapi/extractors.py b/instagrapi/extractors.py index 579d99f6..330c17d3 100644 --- a/instagrapi/extractors.py +++ b/instagrapi/extractors.py @@ -240,6 +240,7 @@ def extract_story_v1(data): story["mentions"] = [ StoryMention(**mention) for mention in story.get("reel_mentions", []) ] + story["locations"] = [] story["hashtags"] = [] story["links"] = [] for cta in story.get("story_cta", []): diff --git a/instagrapi/mixins/photo.py b/instagrapi/mixins/photo.py index ae874451..24d381dd 100644 --- a/instagrapi/mixins/photo.py +++ b/instagrapi/mixins/photo.py @@ -14,7 +14,7 @@ PhotoConfigureStoryError, PhotoNotUpload) from instagrapi.extractors import extract_media_v1 from instagrapi.types import (Location, Media, Story, StoryHashtag, StoryLink, - StoryMention, Usertag) + StoryLocation, StoryMention, Usertag) from instagrapi.utils import dumps try: @@ -263,7 +263,7 @@ def photo_upload_to_story( caption: str, upload_id: str = "", mentions: List[StoryMention] = [], - location: Location = None, + locations: List[StoryLocation] = [], links: List[StoryLink] = [], hashtags: List[StoryHashtag] = [], ) -> Story: @@ -280,8 +280,8 @@ def photo_upload_to_story( Unique upload_id (String). When None, then generate automatically. Example from video.video_configure mentions: List[StoryMention], optional List of mentions to be tagged on this upload, default is empty list. - location: Location, optional - Location tag for this upload, default is None + locations: List[StoryLocation], optional + List of locations to be tagged on this upload, default is empty list. links: List[StoryLink] URLs for Swipe Up hashtags: List[StoryHashtag], optional @@ -303,7 +303,7 @@ def photo_upload_to_story( height, caption, mentions, - location, + locations, links, hashtags, ): @@ -313,6 +313,7 @@ def photo_upload_to_story( links=links, mentions=mentions, hashtags=hashtags, + locations=locations, **extract_media_v1(media).dict() ) raise PhotoConfigureStoryError( @@ -326,7 +327,7 @@ def photo_configure_to_story( height: int, caption: str, mentions: List[StoryMention] = [], - location: Location = None, + locations: List[StoryLocation] = [], links: List[StoryLink] = [], hashtags: List[StoryHashtag] = [], ) -> Dict: @@ -345,8 +346,8 @@ def photo_configure_to_story( Media caption mentions: List[StoryMention], optional List of mentions to be tagged on this upload, default is empty list. - location: Location, optional - Location tag for this upload, default is None + locations: List[StoryLocation], optional + List of locations to be tagged on this upload, default is empty list. links: List[StoryLink] URLs for Swipe Up hashtags: List[StoryHashtag], optional @@ -358,6 +359,7 @@ def photo_configure_to_story( A dictionary of response from the call """ timestamp = int(time.time()) + story_sticker_ids = [] data = { "text_metadata": '[{"font_size":40.0,"scale":1.0,"width":611.0,"height":169.0,"x":0.51414347,"y":0.8487708,"rotation":0.0}]', "supported_capabilities_new": json.dumps(config.SUPPORTED_CAPABILITIES), @@ -366,7 +368,7 @@ def photo_configure_to_story( "scene_capture_type": "", "timezone_offset": "10800", "client_shared_at": str(timestamp - 5), # 5 seconds ago - "story_sticker_ids": "time_sticker_digital", + "story_sticker_ids": "", "media_folder": "Camera", "configure_mode": "1", "source_type": "4", @@ -378,7 +380,6 @@ def photo_configure_to_story( "upload_id": upload_id, "client_timestamp": str(timestamp), "device": self.device, - "implicit_location": {}, "edits": { "crop_original_size": [width * 1.0, height * 1.0], "crop_center": [0.0, 0.0], @@ -386,13 +387,6 @@ def photo_configure_to_story( }, "extra": {"source_width": width, "source_height": height}, } - if location: - assert isinstance(location, Location), \ - f'location must been Location (not {type(location)})' - loc = self.location_build(location) - data["implicit_location"] = { - "media_location": {"lat": loc.lat, "lng": loc.lng} - } if links: links = [link.dict() for link in links] data["story_cta"] = dumps([{"links": links}]) @@ -416,6 +410,7 @@ def photo_configure_to_story( data["reel_mentions"] = json.dumps(reel_mentions) tap_models.extend(reel_mentions) if hashtags: + story_sticker_ids.append("hashtag_sticker") for mention in hashtags: item = { "x": mention.x, @@ -431,7 +426,26 @@ def photo_configure_to_story( "tap_state_str_id": "hashtag_sticker_gradient" } tap_models.append(item) + if locations: + story_sticker_ids.append("location_sticker") + for mention in locations: + mention.location = self.location_complete(mention.location) + item = { + "x": mention.x, + "y": mention.y, + "z": 0, + "width": mention.width, + "height": mention.height, + "rotation": 0.0, + "type": "location", + "location_id": str(mention.location.pk), + "is_sticker": True, + "tap_state": 0, + "tap_state_str_id": "location_sticker_vibrant" + } + tap_models.append(item) data["tap_models"] = dumps(tap_models) + data["story_sticker_ids"] = dumps(story_sticker_ids) return self.private_request( "media/configure_to_story/", self.with_default_data(data) ) diff --git a/instagrapi/mixins/video.py b/instagrapi/mixins/video.py index 2b23a052..1bc6877a 100644 --- a/instagrapi/mixins/video.py +++ b/instagrapi/mixins/video.py @@ -13,7 +13,7 @@ VideoNotUpload) from instagrapi.extractors import extract_media_v1 from instagrapi.types import (Location, Media, Story, StoryHashtag, StoryLink, - StoryMention, Usertag) + StoryLocation, StoryMention, Usertag) from instagrapi.utils import dumps @@ -317,7 +317,7 @@ def video_upload_to_story( caption: str, thumbnail: Path = None, mentions: List[StoryMention] = [], - location: Location = None, + locations: List[StoryLocation] = [], links: List[StoryLink] = [], hashtags: List[StoryHashtag] = [], ) -> Story: @@ -334,8 +334,8 @@ def video_upload_to_story( Path to thumbnail for video. When None, then thumbnail is generate automatically mentions: List[StoryMention], optional List of mentions to be tagged on this upload, default is empty list. - location: Location, optional - Location tag for this upload, default is None + locations: List[StoryLocation], optional + List of locations to be tagged on this upload, default is empty list. links: List[StoryLink] URLs for Swipe Up hashtags: List[StoryHashtag], optional @@ -364,7 +364,7 @@ def video_upload_to_story( thumbnail, caption, mentions, - location, + locations, links, hashtags, ) @@ -384,6 +384,7 @@ def video_upload_to_story( links=links, mentions=mentions, hashtags=hashtags, + locations=locations, **extract_media_v1(media).dict() ) raise VideoConfigureStoryError( @@ -399,7 +400,7 @@ def video_configure_to_story( thumbnail: Path, caption: str, mentions: List[StoryMention] = [], - location: Location = None, + locations: List[StoryLocation] = [], links: List[StoryLink] = [], hashtags: List[StoryHashtag] = [], ) -> Dict: @@ -422,8 +423,8 @@ def video_configure_to_story( Media caption mentions: List[StoryMention], optional List of mentions to be tagged on this upload, default is empty list. - location: Location, optional - Location tag for this upload, default is None + locations: List[StoryLocation], optional + List of locations to be tagged on this upload, default is empty list. links: List[StoryLink] URLs for Swipe Up hashtags: List[StoryHashtag], optional @@ -435,6 +436,7 @@ def video_configure_to_story( A dictionary of response from the call """ timestamp = int(time.time()) + story_sticker_ids = [] data = { "supported_capabilities_new": dumps(config.SUPPORTED_CAPABILITIES), "has_original_sound": "1", @@ -453,6 +455,7 @@ def video_configure_to_story( "client_shared_at": str(timestamp - 7), # 7 seconds ago "imported_taken_at": str(timestamp - 5 * 24 * 3600), # 5 days ago "date_time_original": time.strftime("%Y%m%dT%H%M%S.000Z", time.localtime()), + "story_sticker_ids": "", "media_folder": "Camera", "configure_mode": "1", "source_type": "4", @@ -471,19 +474,11 @@ def video_configure_to_story( # "attempt_id": str(uuid4()), "device": self.device, "length": duration, - "implicit_location": {}, "clips": [{"length": duration, "source_type": "4"}], "extra": {"source_width": width, "source_height": height}, "audio_muted": False, "poster_frame_index": 0, } - if location: - assert isinstance(location, Location), \ - f'location must been Location (not {type(location)})' - loc = self.location_build(location) - data["implicit_location"] = { - "media_location": {"lat": loc.lat, "lng": loc.lng} - } if links: links = [link.dict() for link in links] data["story_cta"] = dumps([{"links": links}]) @@ -521,6 +516,7 @@ def video_configure_to_story( data["reel_mentions"] = dumps(reel_mentions) tap_models.extend(reel_mentions) if hashtags: + story_sticker_ids.append("hashtag_sticker") for mention in hashtags: item = { "x": mention.x, @@ -536,7 +532,26 @@ def video_configure_to_story( "tap_state_str_id": "hashtag_sticker_gradient" } tap_models.append(item) + if locations: + story_sticker_ids.append("location_sticker") + for mention in locations: + mention.location = self.location_complete(mention.location) + item = { + "x": mention.x, + "y": mention.y, + "z": 0, + "width": mention.width, + "height": mention.height, + "rotation": 0.0, + "type": "location", + "location_id": str(mention.location.pk), + "is_sticker": True, + "tap_state": 0, + "tap_state_str_id": "location_sticker_vibrant" + } + tap_models.append(item) data["tap_models"] = dumps(tap_models) + data["story_sticker_ids"] = dumps(story_sticker_ids) return self.private_request( "media/configure_to_story/?video=1", self.with_default_data(data) ) diff --git a/instagrapi/types.py b/instagrapi/types.py index a0b73733..704cd966 100644 --- a/instagrapi/types.py +++ b/instagrapi/types.py @@ -151,6 +151,14 @@ class StoryHashtag(BaseModel): height: Optional[float] +class StoryLocation(BaseModel): + location: Location + x: Optional[float] + y: Optional[float] + width: Optional[float] + height: Optional[float] + + class StoryBuild(BaseModel): mentions: List[StoryMention] path: FilePath @@ -174,6 +182,7 @@ class Story(BaseModel): mentions: List[StoryMention] links: List[StoryLink] hashtags: List[StoryHashtag] + locations: List[StoryLocation] class DirectMessage(BaseModel): diff --git a/setup.py b/setup.py index e206539f..5ffbd1c2 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( name='instagrapi', - version='1.4.2', + version='1.4.3', author='Mikhail Andreev', author_email='x11org@gmail.com', license='MIT', diff --git a/tests.py b/tests.py index bdcf2de7..61ef0297 100644 --- a/tests.py +++ b/tests.py @@ -19,9 +19,10 @@ Media, MediaOembed, Story, + StoryLink, + StoryLocation, StoryMention, StoryHashtag, - StoryLink, User, UserShort, Usertag @@ -1112,18 +1113,28 @@ def test_upload_photo_story(self): self.assertIsInstance(path, Path) caption = 'Test photo caption' adw0rd = self.api.user_info_by_username('adw0rd') - dhbastards = self.api.hashtag_info('dhbastards') self.assertIsInstance(adw0rd, User) mentions = [StoryMention(user=adw0rd)] links = [StoryLink(webUri='https://adw0rd.com/')] - hashtags = [StoryHashtag(hashtag=dhbastards)] + hashtags = [ + StoryHashtag(hashtag=self.api.hashtag_info('dhbastards')) + ] + locations = [ + StoryLocation( + location=Location( + pk=150300262230285, + name='Blaues Wunder (Dresden)', + ) + ) + ] try: story = self.api.photo_upload_to_story( path, caption, mentions=mentions, links=links, - hashtags=hashtags + hashtags=hashtags, + locations=locations, ) self.assertIsInstance(story, Story) self.assertTrue(story) @@ -1139,11 +1150,20 @@ def test_upload_video_story(self): self.assertIsInstance(path, Path) caption = 'Test video caption' adw0rd = self.api.user_info_by_username('adw0rd') - dhbastards = self.api.hashtag_info('dhbastards') self.assertIsInstance(adw0rd, User) mentions = [StoryMention(user=adw0rd)] links = [StoryLink(webUri='https://adw0rd.com/')] - hashtags = [StoryHashtag(hashtag=dhbastards)] + hashtags = [ + StoryHashtag(hashtag=self.api.hashtag_info('dhbastards')) + ] + locations = [ + StoryLocation( + location=Location( + pk=150300262230285, + name='Blaues Wunder (Dresden)', + ) + ) + ] try: buildout = StoryBuilder( path, caption, mentions, @@ -1154,7 +1174,8 @@ def test_upload_video_story(self): caption, mentions=buildout.mentions, links=links, - hashtags=hashtags + hashtags=hashtags, + locations=locations, ) self.assertIsInstance(story, Story) self.assertTrue(story)