Chrunos commited on
Commit
1b86650
·
verified ·
1 Parent(s): 887ec44

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +315 -215
app.py CHANGED
@@ -1,14 +1,15 @@
1
  from fastapi import FastAPI, HTTPException, status, BackgroundTasks
2
- from fastapi.responses import StreamingResponse
3
- import instaloader
4
  import requests
5
  import os
6
  import time
7
  import logging
8
  import random
9
- from functools import wraps
10
- from datetime import datetime, timedelta
11
  import json
 
 
 
 
12
 
13
  # Configure logging
14
  logging.basicConfig(
@@ -23,29 +24,30 @@ app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
23
  INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
24
  INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
25
 
26
- # Session configuration
27
- SESSION_DIR = "/tmp/instagram_sessions"
28
- SESSION_FILE = os.path.join(SESSION_DIR, f"session-{INSTAGRAM_USERNAME}") if INSTAGRAM_USERNAME else None
29
  CACHE_DIR = "/tmp/instagram_cache"
30
-
31
- # Create required directories
32
- os.makedirs(SESSION_DIR, exist_ok=True)
33
  os.makedirs(CACHE_DIR, exist_ok=True)
34
 
35
- # Simple memory cache
36
  STORY_CACHE = {}
37
  CACHE_EXPIRY = {}
38
- LAST_REQUEST = {}
39
- COOLDOWN_UNTIL = {}
 
 
 
40
 
 
41
  USER_AGENTS = [
42
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
43
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15",
44
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
 
45
  ]
46
 
47
  def get_cache_key(username):
48
- """Simple cache key generator"""
49
  return username.lower()
50
 
51
  def get_cached_stories(username):
@@ -81,222 +83,250 @@ def save_to_cache(username, data, minutes=30):
81
 
82
  def is_rate_limited(username):
83
  """Check if we should rate limit this request"""
84
- # Check if in cooldown
85
- if username in COOLDOWN_UNTIL:
86
- if datetime.now() < COOLDOWN_UNTIL[username]:
87
- seconds_left = (COOLDOWN_UNTIL[username] - datetime.now()).total_seconds()
88
- logger.warning(f"{username} in cooldown for {int(seconds_left)}s")
 
89
  return True
90
  else:
91
- # Cooldown expired
92
- COOLDOWN_UNTIL.pop(username)
93
-
94
- # Check request frequency
95
- if username in LAST_REQUEST:
96
- seconds_since = (datetime.now() - LAST_REQUEST[username]).total_seconds()
97
- if seconds_since < 60: # 1 minute minimum between requests
98
- logger.warning(f"Rate limiting {username}: only {int(seconds_since)}s since last request")
99
- return True
100
-
101
- # Update last request time
102
- LAST_REQUEST[username] = datetime.now()
103
  return False
104
 
105
- def set_cooldown(username, minutes=30):
106
- """Set a cooldown period for a username"""
107
- COOLDOWN_UNTIL[username] = datetime.now() + timedelta(minutes=minutes)
108
- logger.warning(f"Setting {minutes}m cooldown for {username}")
 
 
 
 
109
 
110
- def get_instaloader():
111
- """Create and configure Instaloader instance"""
112
- L = instaloader.Instaloader(
113
- sleep=True,
114
- request_timeout=30,
115
- max_connection_attempts=3,
116
- user_agent=random.choice(USER_AGENTS),
117
- download_pictures=False,
118
- download_videos=False,
119
- download_video_thumbnails=False,
120
- download_geotags=False,
121
- download_comments=False,
122
- compress_json=False,
123
- post_metadata_txt_pattern=''
124
- )
125
 
126
- if not INSTAGRAM_USERNAME or not INSTAGRAM_PASSWORD:
127
- logger.error("Instagram credentials not configured")
128
- raise HTTPException(
129
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
130
- detail="Server configuration error"
131
- )
132
-
133
- try:
134
- if SESSION_FILE and os.path.exists(SESSION_FILE):
135
- logger.info(f"Loading session from {SESSION_FILE}")
 
 
 
 
 
 
 
 
 
 
 
 
136
  try:
137
- L.load_session_from_file(INSTAGRAM_USERNAME, SESSION_FILE)
138
- logger.info("Session loaded successfully")
139
- return L
 
 
 
 
140
  except Exception as e:
141
- logger.warning(f"Session load failed: {str(e)}")
142
-
143
- logger.info("Starting fresh login")
144
- time.sleep(random.uniform(1.0, 2.0)) # Small delay before login
145
- L.login(INSTAGRAM_USERNAME, INSTAGRAM_PASSWORD)
146
 
147
- if SESSION_FILE:
148
- L.save_session_to_file(SESSION_FILE)
149
- logger.info(f"Saved new session to {SESSION_FILE}")
150
 
151
- return L
152
-
153
- except Exception as e:
154
- logger.error(f"Login failed: {str(e)}")
155
- raise HTTPException(
156
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
157
- detail="Instagram login failed"
158
- )
159
 
160
- def handle_instagram_errors(func):
161
- @wraps(func)
162
- async def wrapper(*args, **kwargs):
163
- # Extract username if present
164
- username = ""
165
- if len(args) > 1 and hasattr(args[1], '__str__'):
166
- username = str(args[1])
167
-
168
- try:
169
- # Check for rate limiting
170
- if username and is_rate_limited(username):
171
- # Try to serve from cache
172
- cached = get_cached_stories(username)
173
- if cached:
174
- return {**cached, "from_cache": True}
175
- else:
176
- raise HTTPException(
177
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
178
- detail="Rate limit exceeded. Please try again later."
179
- )
180
-
181
- # Execute the handler
182
- response = await func(*args, **kwargs)
183
 
184
- # Cache successful responses
185
- if username and isinstance(response, dict) and "data" in response:
186
- save_to_cache(username, response)
187
-
188
- return response
189
 
190
- except instaloader.exceptions.ConnectionException as e:
191
- error_message = str(e)
192
- logger.error(f"Connection error: {error_message}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- # Handle rate limiting errors
195
- if "429 Too Many Requests" in error_message:
196
- retry_mins = 30 # Default
197
- try:
198
- # Try to extract minutes from error message
199
- if "retried in" in error_message and "minutes" in error_message:
200
- retry_text = error_message.split("retried in")[1].split("minutes")[0].strip()
201
- retry_mins = int(retry_text)
202
- except:
203
- pass
204
-
205
- if username:
206
- set_cooldown(username, retry_mins)
207
-
208
- # Try to serve from cache
209
- if username:
210
- cached = get_cached_stories(username)
211
- if cached:
212
- return {**cached, "from_cache": True}
213
-
214
- raise HTTPException(
215
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
216
- detail=f"Instagram rate limit exceeded. Please try again in {retry_mins} minutes."
217
- )
218
-
219
- elif "404 Not Found" in error_message:
220
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
221
- detail="Profile or stories not found")
 
 
 
 
 
 
 
 
 
222
  else:
223
- raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
224
- detail="Instagram service unavailable")
225
 
226
- except Exception as e:
227
- logger.error(f"Error: {str(e)}")
228
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
229
- detail="Internal server error")
230
- return wrapper
231
 
232
- @app.get("/stories/{username}")
233
- @handle_instagram_errors
234
- async def get_stories(username: str, background_tasks: BackgroundTasks, cached: bool = False):
235
- """Retrieve Instagram stories for a user"""
236
- logger.info(f"Request for @{username} stories")
 
 
237
 
238
- # Return from cache if requested
239
- if cached:
240
- cached_result = get_cached_stories(username)
241
- if cached_result:
242
- return {**cached_result, "from_cache": True}
243
 
244
  try:
245
- # Get Instagram loader
246
- L = get_instaloader()
247
- logger.info("Instagram loader ready")
248
-
249
- # Small delay
250
- time.sleep(random.uniform(1.0, 2.0))
251
-
252
- # Get profile
253
- logger.info(f"Fetching profile for {username}")
254
- try:
255
- profile = instaloader.Profile.from_username(L.context, username)
256
- except Exception as e:
257
- logger.error(f"Profile fetch error: {str(e)}")
258
- raise
259
-
260
- # Check story access
261
- if not profile.has_viewable_story:
262
- logger.warning("No viewable story")
263
- raise HTTPException(
264
- status_code=status.HTTP_404_NOT_FOUND,
265
- detail="No accessible stories for this user"
266
- )
267
-
268
- # Get stories
269
- logger.info("Fetching stories")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  stories = []
271
- for story in L.get_stories(userids=[profile.userid]):
272
- for item in story.get_items():
273
- story_data = {
274
- "id": str(item.mediaid),
275
- "url": item.url,
276
- "type": "video" if item.is_video else "image",
277
- "timestamp": item.date_utc.isoformat()
278
- }
279
-
280
- # Safe attribute access
281
- if item.is_video and hasattr(item, 'view_count'):
282
- try:
283
- story_data["views"] = item.view_count
284
- except:
285
- pass
286
-
287
- stories.append(story_data)
288
-
289
- if not stories:
290
- logger.info("No stories found")
291
- raise HTTPException(
292
- status_code=status.HTTP_404_NOT_FOUND,
293
- detail="No active stories available"
294
- )
295
 
296
- # Save session in background to not delay response
297
- background_tasks.add_task(lambda: L.save_session_to_file(SESSION_FILE) if SESSION_FILE else None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
- logger.info(f"Returning {len(stories)} stories")
300
  result = {
301
  "data": stories,
302
  "count": len(stories),
@@ -305,13 +335,77 @@ async def get_stories(username: str, background_tasks: BackgroundTasks, cached:
305
  }
306
 
307
  return result
308
-
309
  except Exception as e:
310
- logger.error(f"Error: {str(e)}")
 
 
311
  raise
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  @app.get("/download/{url:path}")
314
- @handle_instagram_errors
315
  async def download_media(url: str):
316
  """Download and proxy media content"""
317
  logger.info(f"Download request for URL")
@@ -325,6 +419,7 @@ async def download_media(url: str):
325
  )
326
 
327
  # Configure request
 
328
  headers = {
329
  "User-Agent": random.choice(USER_AGENTS),
330
  "Referer": "https://www.instagram.com/",
@@ -332,7 +427,7 @@ async def download_media(url: str):
332
  }
333
 
334
  # Get media
335
- response = requests.get(url, headers=headers, stream=True, timeout=15)
336
  response.raise_for_status()
337
 
338
  # Determine content type
@@ -390,4 +485,9 @@ def load_cache_from_disk():
390
  # Health check endpoint
391
  @app.get("/health")
392
  async def health_check():
393
- return {"status": "ok", "timestamp": datetime.now().isoformat()}
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException, status, BackgroundTasks
2
+ from fastapi.responses import StreamingResponse, JSONResponse
 
3
  import requests
4
  import os
5
  import time
6
  import logging
7
  import random
 
 
8
  import json
9
+ from datetime import datetime, timedelta
10
+ import hashlib
11
+ import re
12
+ from urllib.parse import quote
13
 
14
  # Configure logging
15
  logging.basicConfig(
 
24
  INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
25
  INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
26
 
27
+ # Configuration
 
 
28
  CACHE_DIR = "/tmp/instagram_cache"
29
+ COOKIE_FILE = "/tmp/instagram_cookies.json"
 
 
30
  os.makedirs(CACHE_DIR, exist_ok=True)
31
 
32
+ # Cache state
33
  STORY_CACHE = {}
34
  CACHE_EXPIRY = {}
35
+ RATE_LIMITS = {}
36
+
37
+ # Session state
38
+ SESSION = None
39
+ SESSION_LAST_REFRESH = None
40
 
41
+ # User agents
42
  USER_AGENTS = [
43
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
44
+ "Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
45
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/116.0.5845.0 Mobile/15E148 Safari/604.1",
46
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
47
  ]
48
 
49
  def get_cache_key(username):
50
+ """Generate a cache key from username"""
51
  return username.lower()
52
 
53
  def get_cached_stories(username):
 
83
 
84
  def is_rate_limited(username):
85
  """Check if we should rate limit this request"""
86
+ # Check rate limits
87
+ key = username.lower()
88
+ if key in RATE_LIMITS:
89
+ if datetime.now() < RATE_LIMITS[key]["until"]:
90
+ seconds = (RATE_LIMITS[key]["until"] - datetime.now()).total_seconds()
91
+ logger.warning(f"Rate limited {username} for {int(seconds)}s")
92
  return True
93
  else:
94
+ # Expired rate limit
95
+ RATE_LIMITS.pop(key)
 
 
 
 
 
 
 
 
 
 
96
  return False
97
 
98
+ def set_rate_limit(username, minutes=15):
99
+ """Set rate limiting for a username"""
100
+ key = username.lower()
101
+ RATE_LIMITS[key] = {
102
+ "until": datetime.now() + timedelta(minutes=minutes),
103
+ "requests": 0
104
+ }
105
+ logger.warning(f"Setting rate limit for {username} for {minutes} minutes")
106
 
107
+ def get_instagram_session():
108
+ """Get or create an Instagram session"""
109
+ global SESSION, SESSION_LAST_REFRESH
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ # Create new session if none exists or it's older than 30 minutes
112
+ if (SESSION is None or
113
+ SESSION_LAST_REFRESH is None or
114
+ (datetime.now() - SESSION_LAST_REFRESH).total_seconds() > 1800):
115
+
116
+ SESSION = requests.Session()
117
+ SESSION.headers.update({
118
+ 'User-Agent': random.choice(USER_AGENTS),
119
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
120
+ 'Accept-Language': 'en-US,en;q=0.5',
121
+ 'Accept-Encoding': 'gzip, deflate, br',
122
+ 'Connection': 'keep-alive',
123
+ 'Upgrade-Insecure-Requests': '1',
124
+ 'Sec-Fetch-Dest': 'document',
125
+ 'Sec-Fetch-Mode': 'navigate',
126
+ 'Sec-Fetch-Site': 'none',
127
+ 'Sec-Fetch-User': '?1',
128
+ 'TE': 'trailers',
129
+ })
130
+
131
+ # Load cookies if available
132
+ if os.path.exists(COOKIE_FILE):
133
  try:
134
+ with open(COOKIE_FILE, 'r') as f:
135
+ cookies = json.load(f)
136
+
137
+ for cookie in cookies:
138
+ SESSION.cookies.set(cookie['name'], cookie['value'])
139
+
140
+ logger.info("Loaded cookies from file")
141
  except Exception as e:
142
+ logger.warning(f"Failed to load cookies: {str(e)}")
 
 
 
 
143
 
144
+ SESSION_LAST_REFRESH = datetime.now()
 
 
145
 
146
+ # Try to log in if we have credentials
147
+ if not any(c.name == 'sessionid' for c in SESSION.cookies) and INSTAGRAM_USERNAME and INSTAGRAM_PASSWORD:
148
+ try:
149
+ login_instagram()
150
+ except Exception as e:
151
+ logger.error(f"Login failed: {str(e)}")
152
+
153
+ return SESSION
154
 
155
+ def save_cookies():
156
+ """Save session cookies to file"""
157
+ if SESSION is None:
158
+ return
159
+
160
+ try:
161
+ cookies = []
162
+ for cookie in SESSION.cookies:
163
+ cookies.append({
164
+ 'name': cookie.name,
165
+ 'value': cookie.value,
166
+ 'domain': cookie.domain,
167
+ 'path': cookie.path
168
+ })
 
 
 
 
 
 
 
 
 
169
 
170
+ with open(COOKIE_FILE, 'w') as f:
171
+ json.dump(cookies, f)
 
 
 
172
 
173
+ logger.info("Saved cookies to file")
174
+ except Exception as e:
175
+ logger.warning(f"Failed to save cookies: {str(e)}")
176
+
177
+ def login_instagram():
178
+ """Log in to Instagram"""
179
+ if not INSTAGRAM_USERNAME or not INSTAGRAM_PASSWORD:
180
+ logger.error("Instagram credentials not configured")
181
+ raise Exception("Instagram credentials required")
182
+
183
+ session = get_instagram_session()
184
+
185
+ # First get the login page to get the CSRF token
186
+ try:
187
+ resp = session.get('https://www.instagram.com/accounts/login/')
188
+ time.sleep(random.uniform(1, 3))
189
+
190
+ # Extract CSRF token
191
+ csrf_token = None
192
+ match = re.search(r'"csrf_token":"(.*?)"', resp.text)
193
+ if match:
194
+ csrf_token = match.group(1)
195
+
196
+ if not csrf_token:
197
+ raise Exception("Failed to get CSRF token")
198
 
199
+ # Prepare login data
200
+ login_data = {
201
+ 'username': INSTAGRAM_USERNAME,
202
+ 'enc_password': f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{INSTAGRAM_PASSWORD}',
203
+ 'queryParams': {},
204
+ 'optIntoOneTap': 'false',
205
+ 'csrfmiddlewaretoken': csrf_token
206
+ }
207
+
208
+ # Update headers for the login request
209
+ session.headers.update({
210
+ 'X-CSRFToken': csrf_token,
211
+ 'X-Requested-With': 'XMLHttpRequest',
212
+ 'Referer': 'https://www.instagram.com/accounts/login/',
213
+ 'Origin': 'https://www.instagram.com'
214
+ })
215
+
216
+ # Wait a bit before login
217
+ time.sleep(random.uniform(2, 4))
218
+
219
+ # Submit login
220
+ login_resp = session.post(
221
+ 'https://www.instagram.com/accounts/login/ajax/',
222
+ data=login_data
223
+ )
224
+
225
+ login_json = login_resp.json()
226
+
227
+ # Check login status
228
+ if login_json.get('authenticated') and login_json.get('status') == 'ok':
229
+ logger.info("Successfully logged in to Instagram")
230
+ save_cookies()
231
+ return True
232
+ else:
233
+ logger.error(f"Login failed: {login_json}")
234
+ if 'two_factor_required' in login_json:
235
+ raise Exception("Two-factor authentication required")
236
  else:
237
+ raise Exception("Login failed")
 
238
 
239
+ except Exception as e:
240
+ logger.error(f"Login error: {str(e)}")
241
+ raise
 
 
242
 
243
+ def get_user_stories(username):
244
+ """Get stories for a username using the mobile API"""
245
+ if is_rate_limited(username):
246
+ cached = get_cached_stories(username)
247
+ if cached:
248
+ return cached
249
+ raise Exception(f"Rate limited for {username}")
250
 
251
+ session = get_instagram_session()
 
 
 
 
252
 
253
  try:
254
+ # First, get the user ID
255
+ user_id = None
256
+ encoded_username = quote(username)
257
+
258
+ # Using the web API to get user info
259
+ user_info_url = f"https://www.instagram.com/api/v1/users/web_profile_info/?username={encoded_username}"
260
+
261
+ session.headers.update({
262
+ 'X-IG-App-ID': '936619743392459', # Common App ID
263
+ 'X-ASBD-ID': '198387',
264
+ 'X-IG-WWW-Claim': '0',
265
+ 'X-Requested-With': 'XMLHttpRequest',
266
+ 'Referer': f'https://www.instagram.com/{username}/',
267
+ })
268
+
269
+ # Random delay to look more like a real user
270
+ time.sleep(random.uniform(1, 2))
271
+
272
+ user_response = session.get(user_info_url)
273
+ user_data = user_response.json()
274
+
275
+ if user_data.get('status') != 'ok':
276
+ if 'message' in user_data and 'wait' in user_data['message'].lower():
277
+ set_rate_limit(username, 30)
278
+ raise Exception(f"Rate limited: {user_data.get('message')}")
279
+ else:
280
+ raise Exception(f"Failed to get user info: {user_data}")
281
+
282
+ user_id = user_data['data']['user']['id']
283
+ logger.info(f"Got user ID for {username}: {user_id}")
284
+
285
+ # Now get the stories
286
+ # Small delay between requests
287
+ time.sleep(random.uniform(2, 3))
288
+
289
+ # Get the stories
290
+ stories_url = f"https://i.instagram.com/api/v1/feed/user/{user_id}/story/"
291
+
292
+ stories_response = session.get(stories_url)
293
+ stories_data = stories_response.json()
294
+
295
+ if stories_data.get('reel') is None:
296
+ raise Exception("No stories found or private account")
297
+
298
+ # Process the stories
299
  stories = []
300
+ items = stories_data.get('reel', {}).get('items', [])
301
+
302
+ for item in items:
303
+ story = {
304
+ "id": item.get('id', ''),
305
+ "type": "video" if item.get('media_type') == 2 else "image",
306
+ "timestamp": datetime.fromtimestamp(item.get('taken_at')).isoformat()
307
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
+ # Get media URL
310
+ if story["type"] == "video":
311
+ # Find the best video version
312
+ if 'video_versions' in item:
313
+ videos = item['video_versions']
314
+ if videos:
315
+ # Get highest quality video
316
+ story["url"] = videos[0]['url']
317
+ # Add view count if available
318
+ if 'view_count' in item:
319
+ story["views"] = item['view_count']
320
+ else:
321
+ # Find the best image version
322
+ if 'image_versions2' in item:
323
+ images = item['image_versions2']['candidates']
324
+ if images:
325
+ # Get highest quality image
326
+ story["url"] = images[0]['url']
327
+
328
+ stories.append(story)
329
 
 
330
  result = {
331
  "data": stories,
332
  "count": len(stories),
 
335
  }
336
 
337
  return result
338
+
339
  except Exception as e:
340
+ logger.error(f"Error getting stories: {str(e)}")
341
+ if 'rate' in str(e).lower() or 'wait' in str(e).lower():
342
+ set_rate_limit(username, 30)
343
  raise
344
 
345
+ @app.get("/stories/{username}")
346
+ async def get_stories(username: str, cached: bool = False):
347
+ """Get Instagram stories for a user"""
348
+ logger.info(f"Request for @{username} stories")
349
+
350
+ # Return from cache if requested or available
351
+ if cached:
352
+ cached_result = get_cached_stories(username)
353
+ if cached_result:
354
+ return {**cached_result, "from_cache": True}
355
+
356
+ try:
357
+ # Check for rate limiting
358
+ if is_rate_limited(username):
359
+ cached_result = get_cached_stories(username)
360
+ if cached_result:
361
+ return {**cached_result, "from_cache": True, "rate_limited": True}
362
+ else:
363
+ raise HTTPException(
364
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
365
+ detail="Rate limit exceeded for this username. Please try again later."
366
+ )
367
+
368
+ # Get the stories
369
+ result = get_user_stories(username)
370
+
371
+ # Cache the successful result
372
+ save_to_cache(username, result)
373
+
374
+ return result
375
+
376
+ except Exception as e:
377
+ error_message = str(e)
378
+ logger.error(f"Error getting stories: {error_message}")
379
+
380
+ # Check for cached version if there's an error
381
+ cached_result = get_cached_stories(username)
382
+ if cached_result:
383
+ logger.info(f"Returning cached result due to error")
384
+ return {**cached_result, "from_cache": True, "error_occurred": True}
385
+
386
+ # Handle specific errors
387
+ if 'rate' in error_message.lower() or 'wait' in error_message.lower():
388
+ raise HTTPException(
389
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
390
+ detail="Instagram rate limit exceeded. Please try again later."
391
+ )
392
+ elif 'private' in error_message.lower() or 'no stories' in error_message.lower():
393
+ raise HTTPException(
394
+ status_code=status.HTTP_404_NOT_FOUND,
395
+ detail="No stories found or private account"
396
+ )
397
+ elif 'login' in error_message.lower() or 'credentials' in error_message.lower():
398
+ raise HTTPException(
399
+ status_code=status.HTTP_401_UNAUTHORIZED,
400
+ detail="Authentication error"
401
+ )
402
+ else:
403
+ raise HTTPException(
404
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
405
+ detail="Error fetching stories"
406
+ )
407
+
408
  @app.get("/download/{url:path}")
 
409
  async def download_media(url: str):
410
  """Download and proxy media content"""
411
  logger.info(f"Download request for URL")
 
419
  )
420
 
421
  # Configure request
422
+ session = get_instagram_session()
423
  headers = {
424
  "User-Agent": random.choice(USER_AGENTS),
425
  "Referer": "https://www.instagram.com/",
 
427
  }
428
 
429
  # Get media
430
+ response = session.get(url, headers=headers, stream=True, timeout=15)
431
  response.raise_for_status()
432
 
433
  # Determine content type
 
485
  # Health check endpoint
486
  @app.get("/health")
487
  async def health_check():
488
+ return {
489
+ "status": "ok",
490
+ "timestamp": datetime.now().isoformat(),
491
+ "cache_size": len(STORY_CACHE),
492
+ "rate_limits": len(RATE_LIMITS)
493
+ }