fix: fetch stories via web API with correct response parsing
- Replace broken reels_tray/reels_media (403 login_required) with per-user web API - Fix response parsing: stories are in reel.items, not root items - Use Chrome UA and web X-IG-App-ID for web API endpoint
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user