diff --git a/instagram_monitor.py b/instagram_monitor.py index a602ffe..01a1441 100644 --- a/instagram_monitor.py +++ b/instagram_monitor.py @@ -217,10 +217,9 @@ class InstagramMonitor: def _get_user_stories(self, user_pk: int) -> list[dict]: """Fetch stories via web API profile endpoint.""" try: - # Try web API with ?__a=1&__d=dis resp = self.session.get( f"https://www.instagram.com/api/v1/feed/user/{user_pk}/story/", - params={"user_id": user_pk, "__a": "1", "__d": "dis"}, + params={"user_id": user_pk}, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "X-IG-App-ID": "936619743392459", @@ -231,10 +230,14 @@ class InstagramMonitor: timeout=30, ) if resp.status_code != 200: + logger.debug(f"Stories {user_pk}: HTTP {resp.status_code} {resp.text[:300]}") return [] data = resp.json() - items = data.get("items", []) + reel = data.get("reel") + if not reel: + return [] + items = reel.get("items", []) if not items: return [] @@ -387,66 +390,49 @@ class InstagramMonitor: followees = self._get_followees() logger.info(f"Checking {len(followees)} followees") - # Method 1: reels_tray for all stories - all_stories = self._get_all_stories() - total_stories = sum(len(s) for s in all_stories.values()) + # Fetch stories per-user via web API (mobile API endpoints are blocked) + logger.info("Fetching stories via web API for each followee...") + semaphore = asyncio.Semaphore(config.concurrent_checks) - # Method 2: per-user stories via reels_media (fallback) - if total_stories == 0: - logger.info("reels_tray empty, trying reels_media for followees...") - # Batch user IDs for reels_media - user_pks = [u["pk"] for u in followees] - for batch_start in range(0, len(user_pks), 20): - batch = user_pks[batch_start:batch_start + 20] - try: - resp = self.session.post( - f"{API_BASE}/feed/reels_media/", - json={"user_ids": batch}, - timeout=30, + async def process_followee_stories(user: dict) -> list[dict]: + username = user["username"] + user_pk = user["pk"] + results = [] + + async with semaphore: + stories = await asyncio.to_thread(self._get_user_stories, user_pk) + + if stories: + logger.info(f"@{username}: {len(stories)} new stories") + + for story in stories: + media_path = await asyncio.to_thread( + self._download_media, story, username, True ) - logger.debug(f"reels_media batch status: {resp.status_code}") - logger.debug(f"reels_media raw: {resp.text[:500]}") - if resp.status_code == 200: - data = resp.json() - items = data.get("items", []) - for item in items: - user_info = item.get("user", {}) - user_pk = user_info.get("pk", item.get("user_pk", 0)) - media_id = str(item.get("pk", item.get("id", ""))) - if self.db.is_downloaded(media_id): - continue - if user_pk not in all_stories: - all_stories[user_pk] = [] - all_stories[user_pk].append(item) - total_stories += 1 - logger.info(f"reels_media batch: {len(items)} stories from {len(data.get('statuses', []))} users") - except Exception as e: - logger.error(f"reels_media batch error: {e}") + if media_path: + media_id = str(story.get("pk", story.get("id", ""))) + shortcode = story.get("code", media_id) + self.db.mark_downloaded(media_id, username, "story", shortcode, media_path) + results.append({ + "type": "story", + "username": username, + "media_path": media_path, + "caption": "", + "shortcode": shortcode, + }) + await asyncio.to_thread(self._random_delay, 0.5, 1.5) - logger.info(f"Total stories found: {total_stories}") + return results - # Send stories - for user_pk, stories in all_stories.items(): - username = next( - (u["username"] for u in followees if u["pk"] == user_pk), - str(user_pk), - ) - for story in stories: - media_path = await asyncio.to_thread( - self._download_media, story, username, True - ) - if media_path: - media_id = str(story.get("pk", story.get("id", ""))) - shortcode = story.get("code", media_id) - self.db.mark_downloaded(media_id, username, "story", shortcode, media_path) - yield { - "type": "story", - "username": username, - "media_path": media_path, - "caption": "", - "shortcode": shortcode, - } - await asyncio.to_thread(self._random_delay, 0.5, 1.5) + story_tasks = [process_followee_stories(user) for user in followees] + story_results = await asyncio.gather(*story_tasks, return_exceptions=True) + + for result in story_results: + if isinstance(result, Exception): + logger.error(f"Followee story task failed: {result}") + continue + for item in result: + yield item # Fetch posts in parallel semaphore = asyncio.Semaphore(config.concurrent_checks)