feat: use reels_tray for stories (single request for all users)
- /feed/reels_tray/ fetches all stories in one API call - Stories sent before posts (they're more time-sensitive) - Per-user story fetch kept as fallback - Much faster: 1 request vs 85 individual requests
This commit is contained in:
@@ -156,8 +156,6 @@ class InstagramMonitor:
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
if resp.status_code != 404:
|
||||
logger.debug(f"Stories {user_pk}: HTTP {resp.status_code}")
|
||||
return []
|
||||
|
||||
data = resp.json()
|
||||
@@ -171,14 +169,49 @@ class InstagramMonitor:
|
||||
if media_id in self._state:
|
||||
continue
|
||||
stories.append(item)
|
||||
|
||||
if stories:
|
||||
logger.info(f"Found {len(stories)} new stories for user {user_pk}")
|
||||
return stories
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching stories for {user_pk}: {e}")
|
||||
return []
|
||||
|
||||
def _get_all_stories(self) -> dict[int, list[dict]]:
|
||||
"""Fetch all stories from reels_tray (faster, single request)."""
|
||||
user_stories: dict[int, list[dict]] = {}
|
||||
try:
|
||||
resp = self.session.get(
|
||||
f"{API_BASE}/feed/reels_tray/",
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
logger.warning(f"reels_tray: HTTP {resp.status_code} - {resp.text[:200]}")
|
||||
return user_stories
|
||||
|
||||
data = resp.json()
|
||||
trays = data.get("tray", [])
|
||||
logger.info(f"reels_tray: found {len(trays)} users with stories")
|
||||
|
||||
for tray in trays:
|
||||
user_info = tray.get("user", {})
|
||||
user_pk = user_info.get("pk", 0)
|
||||
if not user_pk:
|
||||
continue
|
||||
|
||||
items = tray.get("items", [])
|
||||
stories = []
|
||||
for item in items:
|
||||
media_id = str(item.get("pk", item.get("id", "")))
|
||||
if media_id in self._state:
|
||||
continue
|
||||
stories.append(item)
|
||||
|
||||
if stories:
|
||||
user_stories[user_pk] = stories
|
||||
|
||||
return user_stories
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching reels_tray: {e}")
|
||||
return user_stories
|
||||
|
||||
def _get_user_posts(self, user_pk: int) -> list[dict]:
|
||||
try:
|
||||
resp = self.session.get(
|
||||
@@ -283,40 +316,44 @@ class InstagramMonitor:
|
||||
followees = self._get_followees()
|
||||
logger.info(f"Checking {len(followees)} followees")
|
||||
|
||||
# Fetch all stories at once via reels_tray
|
||||
all_stories = self._get_all_stories()
|
||||
total_stories = sum(len(s) for s in all_stories.values())
|
||||
logger.info(f"Found {total_stories} new stories from {len(all_stories)} users")
|
||||
|
||||
# Send stories first
|
||||
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", "")))
|
||||
self._state[media_id] = datetime.utcnow().isoformat()
|
||||
self._save_state()
|
||||
yield {
|
||||
"type": "story",
|
||||
"username": username,
|
||||
"media_path": media_path,
|
||||
"caption": "",
|
||||
"shortcode": media_id,
|
||||
}
|
||||
await asyncio.to_thread(self._random_delay, 0.5, 1.5)
|
||||
|
||||
# Then fetch posts in parallel
|
||||
semaphore = asyncio.Semaphore(config.concurrent_checks)
|
||||
|
||||
async def process_followee(user: dict) -> list[dict]:
|
||||
async def process_followee_posts(user: dict) -> list[dict]:
|
||||
username = user["username"]
|
||||
user_pk = user["pk"]
|
||||
results = []
|
||||
|
||||
async with semaphore:
|
||||
self._random_delay(1, 3)
|
||||
|
||||
# Fetch stories and posts in parallel
|
||||
stories_task = asyncio.to_thread(self._get_user_stories, user_pk)
|
||||
posts_task = asyncio.to_thread(self._get_user_posts, user_pk)
|
||||
stories, posts = await asyncio.gather(stories_task, posts_task)
|
||||
|
||||
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
|
||||
)
|
||||
if media_path:
|
||||
media_id = str(story.get("pk", story.get("id", "")))
|
||||
self._state[media_id] = datetime.utcnow().isoformat()
|
||||
self._save_state()
|
||||
results.append({
|
||||
"type": "story",
|
||||
"username": username,
|
||||
"media_path": media_path,
|
||||
"caption": "",
|
||||
"shortcode": media_id,
|
||||
})
|
||||
await asyncio.to_thread(self._random_delay, 0.5, 1.5)
|
||||
posts = await asyncio.to_thread(self._get_user_posts, user_pk)
|
||||
|
||||
if posts:
|
||||
logger.info(f"@{username}: {len(posts)} new posts")
|
||||
@@ -342,8 +379,7 @@ class InstagramMonitor:
|
||||
|
||||
return results
|
||||
|
||||
# Process all followees in parallel (with semaphore limit)
|
||||
tasks = [process_followee(user) for user in followees]
|
||||
tasks = [process_followee_posts(user) for user in followees]
|
||||
done_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for result in done_results:
|
||||
|
||||
Reference in New Issue
Block a user