def collection_medias(self, collection_pk: str, amount: int = 21, last_media_pk: int = 0) -> List[Media]: """ Get media in a collection by collection_pk Parameters ---------- collection_pk: str Unique identifier of a Collection amount: int, optional Maximum number of media to return, default is 21 last_media_pk: int, optional Last PK user has seen, function will return medias after this pk. Default is 0 Returns ------- List[Media] A list of objects of Media """ if isinstance(collection_pk, int) or collection_pk.isdigit(): private_request_endpoint = f"feed/collection/{collection_pk}/" elif collection_pk.lower() == "liked": private_request_endpoint = "feed/liked/" else: private_request_endpoint = "feed/saved/posts/" last_media_pk = last_media_pk and int(last_media_pk) total_items = [] next_max_id = "" amount = int(amount) found_last_media_pk = False while True: try: result = self.private_request( private_request_endpoint, params={ "include_igtv_preview": "false", "max_id": next_max_id }, ) except Exception as e: self.logger.exception(e) break for item in result["items"]: if last_media_pk and last_media_pk == item["media"]["pk"]: found_last_media_pk = True break total_items.append(extract_media_v1(item.get("media", item))) if (amount and len(total_items) >= amount) or found_last_media_pk: break if not result.get("more_available"): break next_max_id = result.get("next_max_id", "") return total_items[:amount] if amount else total_items
def hashtag_medias_v1_chunk( self, name: str, max_amount: int = 27, tab_key: str = "", max_id: str = None ) -> Tuple[List[Media], str]: """ Get chunk of medias for a hashtag and max_id (cursor) by Private Mobile API Parameters ---------- name: str Name of the hashtag max_amount: int, optional Maximum number of media to return, default is 27 tab_key: str, optional Tab Key, default value is "" max_id: str Max ID, default value is None Returns ------- Tuple[List[Media], str] List of objects of Media and max_id """ assert tab_key in ("top", "recent"), \ 'You must specify one of the options for "tab_key" ("top" or "recent")' data = { "supported_tabs": dumps([tab_key]), # 'lat': 59.8626416, # 'lng': 30.5126682, "include_persistent": "true", "rank_token": self.rank_token, "count": 10000, } medias = [] while True: result = self.private_request( f"tags/{name}/sections/", params={"max_id": max_id} if max_id else {}, data=self.with_default_data(data), ) for section in result["sections"]: layout_content = section.get("layout_content") or {} nodes = layout_content.get("medias") or [] for node in nodes: if max_amount and len(medias) >= max_amount: break media = extract_media_v1(node["media"]) # check contains hashtag in caption if f"#{name}" not in media.caption_text: continue medias.append(media) if not result["more_available"]: break if max_amount and len(medias) >= max_amount: break max_id = result["next_max_id"] return medias, max_id
def hashtag_medias_v1(self, name: str, amount: int = 27, tab_key: str = "") -> List[Media]: """ Get medias for a hashtag Parameters ---------- name: str Name of the hashtag amount: int, optional Maximum number of media to return, default is 27 tab_key: str, optional Tab Key, default value is "" Returns ------- List[Media] List of objects of Media """ data = { "supported_tabs": dumps([tab_key]), # 'lat': 59.8626416, # 'lng': 30.5126682, "include_persistent": "true", "rank_token": self.rank_token, } max_id = None medias = [] while True: result = self.private_request( f"tags/{name}/sections/", params={"max_id": max_id} if max_id else {}, data=self.with_default_data(data), ) for section in result["sections"]: layout_content = section.get("layout_content") or {} nodes = layout_content.get("medias") or [] for node in nodes: if amount and len(medias) >= amount: break media = extract_media_v1(node["media"]) # check contains hashtag in caption if f"#{name}" not in media.caption_text: continue medias.append(media) if not result["more_available"]: break if amount and len(medias) >= amount: break max_id = result["next_max_id"] if amount: medias = medias[:amount] return medias
def photo_upload( self, path: Path, caption: str, upload_id: str = "", usertags: List[Usertag] = [], location: Location = None, links: List[StoryLink] = [], configure_timeout: int = 3, configure_handler=None, configure_exception=None, ) -> Media: """ Upload photo and configure to feed Parameters ---------- path: Path Path to the media caption: str Media caption upload_id: str, optional Unique upload_id (String). When None, then generate automatically. Example from video.video_configure usertags: List[Usertag], optional List of users to be tagged on this upload, default is empty list. location: Location, optional Location tag for this upload, default is None links: List[StoryLink] URLs for Swipe Up configure_timeout: int Timeout between attempt to configure media (set caption, etc), default is 3 configure_handler Configure handler method, default is None configure_exception Configure exception class, default is None Returns ------- Media An object of Media class """ path = Path(path) upload_id, width, height = self.photo_rupload(path, upload_id) for attempt in range(10): self.logger.debug(f"Attempt #{attempt} to configure Photo: {path}") time.sleep(configure_timeout) if (configure_handler or self.photo_configure)( upload_id, width, height, caption, usertags, location, links ): media = self.last_json.get("media") self.expose() return extract_media_v1(media) raise (configure_exception or PhotoConfigureError)( response=self.last_response, **self.last_json )
def reels_timeline_media(self, collection_pk: str, amount: int = 10, last_media_pk: int = 0) -> List[Media]: """ Get reels timeline media in a collection Parameters ---------- collection_pk: str Unique identifier of a timeline amount: int, optional Maximum number of media to return, default is 10 last_media_pk: int, optional Last PK user has seen, function will return medias after this pk. Default is 0 Returns ------- List[Media] A list of objects of Media """ if collection_pk == "reels": private_request_endpoint = "clips/connected/" elif collection_pk == 'explore_reels': private_request_endpoint = "clips/discover/" last_media_pk = last_media_pk and int(last_media_pk) total_items = [] next_max_id = "" while True: if len(total_items) >= float(amount): return total_items[:amount] try: result = self.private_request( private_request_endpoint, data=' ', params={"max_id": next_max_id}, ) except Exception as e: self.logger.exception(e) return total_items for item in result["items"]: if last_media_pk and last_media_pk == item["media"]["pk"]: return total_items total_items.append(extract_media_v1(item.get("media"))) if not result.get('paging_info', {}).get("more_available"): return total_items next_max_id = result.get('paging_info', {}).get("more_available")
def location_medias_v1_chunk( self, location_pk: int, max_amount: int = 63, tab_key: str = "", max_id: str = None) -> Tuple[List[Media], str]: """ Get chunk of medias for a location and max_id (cursor) by Private Mobile API Parameters ---------- location_pk: int Unique identifier for a location max_amount: int, optional Maximum number of media to return, default is 27 tab_key: str, optional Tab Key, default value is "" max_id: str Max ID, default value is None Returns ------- Tuple[List[Media], str] List of objects of Media and max_id """ assert tab_key in tab_keys_v1, f'You must specify one of the options for "tab_key" {tab_keys_a1}' data = { "_uuid": self.uuid, "session_id": self.client_session_id, "tab": tab_key } medias = [] while True: result = self.private_request( f"locations/{location_pk}/sections/", params={"max_id": max_id} if max_id else {}, data=data, ) for section in result["sections"]: layout_content = section.get("layout_content") or {} nodes = layout_content.get("medias") or [] for node in nodes: if max_amount and len(medias) >= max_amount: break media = extract_media_v1(node["media"]) medias.append(media) if not result["more_available"]: break if max_amount and len(medias) >= max_amount: break max_id = result["next_max_id"] return medias, max_id
def collection_medias(self, collection_pk: int, amount: int = 21, last_media_pk: int = 0) -> List[Media]: """ Get media in a collection by collection pk Parameters ---------- collection_pk: int Unique identifier of a Collection amount: int, optional Maximum number of media to return, default is 21 last_media_pk: int, optional Last PK user has seen, function will return medias after this pk. Default is 0 Returns ------- List[Media] A list of objects of Media """ collection_pk = int(collection_pk) last_media_pk = last_media_pk and int(last_media_pk) total_items = [] next_max_id = "" while True: if len(total_items) >= float(amount): return total_items[:amount] try: result = self.private_request( f"feed/collection/{collection_pk}/", params={ "include_igtv_preview": "false", "max_id": next_max_id }, ) except Exception as e: self.logger.exception(e) return total_items for item in result["items"]: if last_media_pk and last_media_pk == item["media"]["pk"]: return total_items total_items.append(extract_media_v1(item["media"])) if not result.get("more_available"): return total_items next_max_id = result.get("next_max_id", "") return total_items
def photo_upload( self, path: Path, caption: str, upload_id: str = "", usertags: List[Usertag] = [], location: Location = None, ) -> Media: """ Upload photo and configure to feed Parameters ---------- path: Path Path to the media caption: str Media caption upload_id: str, optional Unique upload_id (String). When None, then generate automatically. Example from video.video_configure usertags: List[Usertag], optional List of users to be tagged on this upload, default is empty list. location: Location, optional Location tag for this upload, default is None Returns ------- Media An object of Media class """ path = Path(path) upload_id, width, height = self.photo_rupload(path, upload_id) for attempt in range(10): self.logger.debug(f"Attempt #{attempt} to configure Photo: {path}") time.sleep(3) if self.photo_configure( upload_id, width, height, caption, usertags, location, ): media = self.last_json.get("media") self.expose() return extract_media_v1(media) raise PhotoConfigureError(response=self.last_response, **self.last_json)
def user_medias_paginated_v1( self, user_id: int, amount: int = 0, end_cursor: str = "") -> Tuple[List[Media], str]: """ Get a page of user's media by Private Mobile API Parameters ---------- user_id: int amount: int, optional Maximum number of media to return, default is 0 (all medias) end_cursor: str, optional Cursor value to start at, obtained from previous call to this method Returns ------- Tuple[List[Media], str] A tuple containing a list of medias and the next end_cursor value """ amount = int(amount) user_id = int(user_id) medias = [] next_max_id = end_cursor min_timestamp = None try: items = self.private_request( f"feed/user/{user_id}/", params={ "max_id": next_max_id, "count": 1000, "min_timestamp": min_timestamp, "rank_token": self.rank_token, "ranked_content": "true", }, )["items"] except Exception as e: self.logger.exception(e) return [], None medias.extend(items) next_max_id = self.last_json.get("next_max_id", "") if amount: medias = medias[:amount] return ([extract_media_v1(media) for media in medias], next_max_id)
def user_medias_v1(self, user_id: int, amount: int = 18) -> List[Media]: """ Get a user's media Parameters ---------- user_id: int amount: int, optional Maximum number of media to return, default is 18 Returns ------- List[Media] A list of objects of Media """ amount = int(amount) user_id = int(user_id) medias = [] next_max_id = "" min_timestamp = None while True: try: items = self.private_request( f"feed/user/{user_id}/", params={ "max_id": next_max_id, "min_timestamp": min_timestamp, "rank_token": self.rank_token, "ranked_content": "true", }, )["items"] except Exception as e: self.logger.exception(e) break medias.extend(items) if not self.last_json.get("more_available"): break if len(medias) >= amount: break next_max_id = self.last_json.get("next_max_id", "") return [extract_media_v1(media) for media in medias[:amount]]
def media_info_v1(self, media_pk: int) -> Media: """ Get Media from PK Parameters ---------- media_pk: int Unique identifier of the media Returns ------- Media An object of Media type """ try: result = self.private_request(f"media/{media_pk}/info/") except ClientNotFoundError as e: raise MediaNotFound(e, media_pk=media_pk, **self.last_json) except ClientError as e: if "Media not found" in str(e): raise MediaNotFound(e, media_pk=media_pk, **self.last_json) raise e return extract_media_v1(result["items"].pop())
def usertag_medias_v1(self, user_id: int, amount: int = 0) -> List[Media]: """ Get medias where a user is tagged (by Private Mobile API) Parameters ---------- user_id: int amount: int, optional Maximum number of media to return, default is 0 (all medias) Returns ------- List[Media] A list of objects of Media """ amount = int(amount) user_id = int(user_id) medias = [] next_max_id = "" while True: try: items = self.private_request(f"usertags/{user_id}/feed/", params={"max_id": next_max_id})["items"] except Exception as e: self.logger.exception(e) break medias.extend(items) if not self.last_json.get("more_available"): break if amount and len(medias) >= amount: break next_max_id = self.last_json.get("next_max_id", "") if amount: medias = medias[:amount] return [extract_media_v1(media) for media in medias]
def album_upload( self, paths: List[Path], caption: str, usertags: List[Usertag] = [], location: Location = None, configure_timeout: int = 3, configure_handler=None, configure_exception=None, to_story=False, ) -> Media: """ Upload album to feed Parameters ---------- paths: List[Path] List of paths for media to upload caption: str Media caption usertags: List[Usertag], optional List of users to be tagged on this upload, default is empty list. location: Location, optional Location tag for this upload, default is none configure_timeout: int Timeout between attempt to configure media (set caption, etc), default is 3 configure_handler Configure handler method, default is None configure_exception Configure exception class, default is None to_story: bool Currently not used, default is False Returns ------- Media An object of Media class """ children = [] for path in paths: path = Path(path) if path.suffix == ".jpg": upload_id, width, height = self.photo_rupload(path, to_album=True) children.append( { "upload_id": upload_id, "edits": dumps( { "crop_original_size": [width, height], "crop_center": [0.0, -0.0], "crop_zoom": 1.0, } ), "extra": dumps( {"source_width": width, "source_height": height} ), "scene_capture_type": "", "scene_type": None, } ) elif path.suffix == ".mp4": upload_id, width, height, duration, thumbnail = self.video_rupload( path, to_album=True ) children.append( { "upload_id": upload_id, "clips": dumps([{"length": duration, "source_type": "4"}]), "extra": dumps( {"source_width": width, "source_height": height} ), "length": duration, "poster_frame_index": "0", "filter_type": "0", "video_result": "", "date_time_original": time.strftime( "%Y%m%dT%H%M%S.000Z", time.localtime() ), "audio_muted": "false", } ) self.photo_rupload(thumbnail, upload_id) else: raise AlbumUnknownFormat() for attempt in range(20): self.logger.debug(f"Attempt #{attempt} to configure Album: {paths}") time.sleep(configure_timeout) try: configured = (configure_handler or self.album_configure)( children, caption, usertags, location ) except Exception as e: if "Transcode not finished yet" in str(e): """ Response 202 status: {"message": "Transcode not finished yet.", "status": "fail"} """ time.sleep(10) continue raise e else: if configured: media = configured.get("media") self.expose() return extract_media_v1(media) raise (configure_exception or AlbumConfigureError)( response=self.last_response, **self.last_json )
def video_upload( self, path: Path, caption: str, thumbnail: Path = None, usertags: List[Usertag] = [], location: Location = None, ) -> Media: """ Upload video and configure to feed Parameters ---------- path: Path Path to the media caption: str Media caption thumbnail: str Path to thumbnail for video. When None, then thumbnail is generate automatically usertags: List[Usertag], optional List of users to be tagged on this upload, default is empty list. location: Location, optional Location tag for this upload, default is None Returns ------- Media An object of Media class """ path = Path(path) if thumbnail is not None: thumbnail = Path(thumbnail) upload_id, width, height, duration, thumbnail = self.video_rupload( path, thumbnail, to_story=False) for attempt in range(20): self.logger.debug(f"Attempt #{attempt} to configure Video: {path}") time.sleep(3) try: configured = self.video_configure( upload_id, width, height, duration, thumbnail, caption, usertags, location, ) except Exception as e: if "Transcode not finished yet" in str(e): """ Response 202 status: {"message": "Transcode not finished yet.", "status": "fail"} """ time.sleep(10) continue raise e else: if configured: media = configured.get("media") self.expose() return extract_media_v1(media) raise VideoConfigureError(response=self.last_response, **self.last_json)
def video_upload( self, path: Path, caption: str, thumbnail: Path = None, usertags: List[Usertag] = [], location: Location = None, links: List[StoryLink] = [], configure_timeout: int = 3, configure_handler=None, configure_exception=None, to_story: bool = False, ) -> Media: """ Upload video and configure to feed Parameters ---------- path: Path Path to the media caption: str Media caption thumbnail: str Path to thumbnail for video. When None, then thumbnail is generate automatically usertags: List[Usertag], optional List of users to be tagged on this upload, default is empty list. location: Location, optional Location tag for this upload, default is None links: List[StoryLink] URLs for Swipe Up configure_timeout: int Timeout between attempt to configure media (set caption, etc), default is 3 configure_handler Configure handler method, default is None configure_exception Configure exception class, default is None to_story: bool, optional Returns ------- Media An object of Media class """ path = Path(path) if thumbnail is not None: thumbnail = Path(thumbnail) upload_id, width, height, duration, thumbnail = self.video_rupload( path, thumbnail, to_story=to_story) for attempt in range(20): self.logger.debug(f"Attempt #{attempt} to configure Video: {path}") time.sleep(configure_timeout) try: configured = (configure_handler or self.video_configure)( upload_id, width, height, duration, thumbnail, caption, usertags, location, links, ) except Exception as e: if "Transcode not finished yet" in str(e): """ Response 202 status: {"message": "Transcode not finished yet.", "status": "fail"} """ time.sleep(10) continue raise e else: if configured: media = configured.get("media") self.expose() return extract_media_v1(media) raise (configure_exception or VideoConfigureError)(response=self.last_response, **self.last_json)
def video_upload_to_story( self, path: Path, caption: str, thumbnail: Path = None, mentions: List[StoryMention] = [], locations: List[StoryLocation] = [], links: List[StoryLink] = [], hashtags: List[StoryHashtag] = [], ) -> Story: """ Upload video as a story and configure it Parameters ---------- path: Path Path to the media caption: str Media caption thumbnail: str 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. 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 List of hashtags to be tagged on this upload, default is empty list. Returns ------- Story An object of Media class """ path = Path(path) if thumbnail is not None: thumbnail = Path(thumbnail) upload_id, width, height, duration, thumbnail = self.video_rupload( path, thumbnail, to_story=True) for attempt in range(20): self.logger.debug(f"Attempt #{attempt} to configure Video: {path}") time.sleep(3) try: configured = self.video_configure_to_story( upload_id, width, height, duration, thumbnail, caption, mentions, locations, links, hashtags, ) except Exception as e: if "Transcode not finished yet" in str(e): """ Response 202 status: {"message": "Transcode not finished yet.", "status": "fail"} """ time.sleep(10) continue raise e if configured: media = configured.get("media") self.expose() return Story(links=links, mentions=mentions, hashtags=hashtags, locations=locations, **extract_media_v1(media).dict()) raise VideoConfigureStoryError(response=self.last_response, **self.last_json)
def igtv_upload( self, path: Path, title: str, caption: str, thumbnail: Path = None, usertags: List[Usertag] = [], location: Location = None, configure_timeout: int = 10, ) -> Media: """ Upload IGTV to Instagram Parameters ---------- path: Path Path to IGTV file title: str Title of the video caption: str Media caption thumbnail: Path, optional Path to thumbnail for IGTV. Default value is None, and it generates a thumbnail usertags: List[Usertag], optional List of users to be tagged on this upload, default is empty list. location: Location, optional Location tag for this upload, default is none configure_timeout: int Timeout between attempt to configure media (set caption, etc), default is 10 Returns ------- Media An object of Media class """ path = Path(path) if thumbnail is not None: thumbnail = Path(thumbnail) upload_id = str(int(time.time() * 1000)) thumbnail, width, height, duration = analyze_video(path, thumbnail) waterfall_id = str(uuid4()) # upload_name example: '1576102477530_0_7823256191' upload_name = "{upload_id}_0_{rand}".format(upload_id=upload_id, rand=random.randint( 1000000000, 9999999999)) # by segments bb2c1d0c127384453a2122e79e4c9a85-0-6498763 # upload_name = "{hash}-0-{rand}".format( # hash="bb2c1d0c127384453a2122e79e4c9a85", rand=random.randint(1111111, 9999999) # ) rupload_params = { "is_igtv_video": "1", "retry_context": '{"num_step_auto_retry":0,"num_reupload":0,"num_step_manual_retry":0}', "media_type": "2", "xsharing_user_ids": json.dumps([self.user_id]), "upload_id": upload_id, "upload_media_duration_ms": str(int(duration * 1000)), "upload_media_width": str(width), "upload_media_height": str(height), } headers = { "Accept-Encoding": "gzip", "X-Instagram-Rupload-Params": json.dumps(rupload_params), "X_FB_VIDEO_WATERFALL_ID": waterfall_id, "X-Entity-Type": "video/mp4", } response = self.private.get( "https://{domain}/rupload_igvideo/{name}".format( domain=config.API_DOMAIN, name=upload_name), headers=headers, ) self.request_log(response) if response.status_code != 200: raise IGTVNotUpload(response=self.last_response, **self.last_json) igtv_data = open(path, "rb").read() igtv_len = str(len(igtv_data)) headers = { "Offset": "0", "X-Entity-Name": upload_name, "X-Entity-Length": igtv_len, "Content-Type": "application/octet-stream", "Content-Length": igtv_len, **headers, } response = self.private.post( "https://{domain}/rupload_igvideo/{name}".format( domain=config.API_DOMAIN, name=upload_name), data=igtv_data, headers=headers, ) self.request_log(response) if response.status_code != 200: raise IGTVNotUpload(response=self.last_response, **self.last_json) # CONFIGURE self.igtv_composer_session_id = self.generate_uuid() for attempt in range(20): self.logger.debug(f"Attempt #{attempt} to configure IGTV: {path}") time.sleep(configure_timeout) try: configured = self.igtv_configure( upload_id, thumbnail, width, height, duration, title, caption, usertags, location, ) except ClientError as e: if "Transcode not finished yet" in str(e): """ Response 202 status: {"message": "Transcode not finished yet.", "status": "fail"} """ time.sleep(10) continue raise e else: if configured: media = self.last_json.get("media") self.expose() return extract_media_v1(media) raise IGTVConfigureError(response=self.last_response, **self.last_json)
def photo_upload_to_story( self, path: Path, caption: str, upload_id: str = "", mentions: List[StoryMention] = [], locations: List[StoryLocation] = [], links: List[StoryLink] = [], hashtags: List[StoryHashtag] = [], stickers: List[StorySticker] = [], ) -> Story: """ Upload photo as a story and configure it Parameters ---------- path: Path Path to the media caption: str Media caption upload_id: str, optional 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. 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 List of hashtags to be tagged on this upload, default is empty list. stickers: List[StorySticker], optional List of stickers to be tagged on this upload, default is empty list. Returns ------- Story An object of Media class """ path = Path(path) upload_id, width, height = self.photo_rupload(path, upload_id) for attempt in range(10): self.logger.debug(f"Attempt #{attempt} to configure Photo: {path}") time.sleep(3) if self.photo_configure_to_story( upload_id, width, height, caption, mentions, locations, links, hashtags, stickers, ): media = self.last_json.get("media") self.expose() return Story(links=links, mentions=mentions, hashtags=hashtags, locations=locations, stickers=stickers, **extract_media_v1(media).dict()) raise PhotoConfigureStoryError(response=self.last_response, **self.last_json)