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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -219
app.py CHANGED
@@ -7,8 +7,8 @@ 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
@@ -20,13 +20,8 @@ logger = logging.getLogger(__name__)
20
 
21
  app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
22
 
23
- # Environment variables
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
@@ -34,16 +29,11 @@ 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):
@@ -104,242 +94,176 @@ def set_rate_limit(username, minutes=15):
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),
333
  "username": username,
334
- "fetched_at": datetime.now().isoformat()
 
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}")
@@ -365,14 +289,58 @@ async def get_stories(username: str, cached: bool = False):
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}")
@@ -384,20 +352,15 @@ async def get_stories(username: str, cached: bool = False):
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(
@@ -408,18 +371,18 @@ async def get_stories(username: str, cached: bool = False):
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")
412
 
413
  try:
414
  # Validate URL
415
- if not url.startswith(("https://instagram", "https://scontent")):
416
  raise HTTPException(
417
  status_code=status.HTTP_400_BAD_REQUEST,
418
  detail="Invalid URL format"
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/",
 
7
  import random
8
  import json
9
  from datetime import datetime, timedelta
 
10
  import re
11
+ import base64
12
  from urllib.parse import quote
13
 
14
  # Configure logging
 
20
 
21
  app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
22
 
 
 
 
 
23
  # Configuration
24
  CACHE_DIR = "/tmp/instagram_cache"
 
25
  os.makedirs(CACHE_DIR, exist_ok=True)
26
 
27
  # Cache state
 
29
  CACHE_EXPIRY = {}
30
  RATE_LIMITS = {}
31
 
 
 
 
 
32
  # User agents
33
  USER_AGENTS = [
34
  "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",
35
  "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",
36
+ "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"
 
37
  ]
38
 
39
  def get_cache_key(username):
 
94
  }
95
  logger.warning(f"Setting rate limit for {username} for {minutes} minutes")
96
 
97
+ def get_random_delay():
98
+ """Get a random delay time to mimic human behavior"""
99
+ return random.uniform(1, 3)
100
+
101
+ def fetch_stories_from_web(username):
102
+ """Fetch stories using the public web interface"""
103
+ url = f"https://storiesig.info/api/ig/stories/{username}"
104
 
105
+ try:
106
+ session = requests.Session()
107
+ session.headers.update({
 
 
 
 
108
  'User-Agent': random.choice(USER_AGENTS),
109
+ 'Accept': 'application/json, text/plain, */*',
110
  'Accept-Language': 'en-US,en;q=0.5',
111
+ 'Referer': f'https://storiesig.info/stories/{username}',
112
+ 'Origin': 'https://storiesig.info',
113
+ 'Sec-Fetch-Dest': 'empty',
114
+ 'Sec-Fetch-Mode': 'cors',
115
+ 'Sec-Fetch-Site': 'same-origin',
 
 
 
116
  })
117
 
118
+ # Add random delay to mimic human behavior
119
+ time.sleep(get_random_delay())
 
 
 
 
 
 
 
 
 
 
120
 
121
+ response = session.get(url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ if response.status_code == 429:
124
+ logger.warning(f"Rate limited by storiesig.info for {username}")
125
+ set_rate_limit(username, 10)
126
+ raise Exception("Rate limited by service")
 
 
 
 
 
 
 
 
127
 
128
+ response.raise_for_status()
129
+ data = response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ if not data.get('stories'):
132
+ logger.warning(f"No stories found for {username}")
133
+ raise Exception("No stories found")
 
 
134
 
135
+ # Process the stories
136
+ stories = []
137
+ for item in data.get('stories', []):
138
+ story = {
139
+ "id": item.get('id', ''),
140
+ "type": item.get('type', 'image'),
141
+ "timestamp": datetime.fromtimestamp(item.get('taken_at_timestamp', 0)).isoformat(),
142
+ "url": item.get('url', '')
143
+ }
144
 
145
+ # Add thumbnail if available
146
+ if item.get('thumbnail_url'):
147
+ story["thumbnail"] = item.get('thumbnail_url')
148
+
149
+ stories.append(story)
150
+
151
+ result = {
152
+ "data": stories,
153
+ "count": len(stories),
154
+ "username": username,
155
+ "fetched_at": datetime.now().isoformat()
156
  }
157
 
158
+ return result
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error fetching stories from web: {str(e)}")
162
+ raise
163
+
164
+ def fetch_stories_from_alternative(username):
165
+ """Try an alternative source for stories"""
166
+ url = f"https://instastories.watch/api/stories/{username}"
167
+
168
+ try:
169
+ session = requests.Session()
170
  session.headers.update({
171
+ 'User-Agent': random.choice(USER_AGENTS),
172
+ 'Accept': 'application/json',
173
+ 'Referer': f'https://instastories.watch/stories/{username}/',
174
+ 'Origin': 'https://instastories.watch'
175
  })
176
 
177
+ # Add random delay
178
+ time.sleep(get_random_delay())
179
 
180
+ response = session.get(url)
181
+ response.raise_for_status()
182
+ data = response.json()
 
 
183
 
184
+ if not data or not data.get('stories'):
185
+ logger.warning(f"No stories found for {username} on alternative source")
186
+ raise Exception("No stories found")
187
 
188
+ # Process the stories
189
+ stories = []
190
+ for item in data.get('stories', []):
191
+ story = {
192
+ "id": item.get('id', ''),
193
+ "type": "video" if item.get('is_video', False) else "image",
194
+ "timestamp": datetime.fromtimestamp(item.get('taken_at', 0)).isoformat(),
195
+ "url": item.get('media_url', '')
196
+ }
197
+
198
+ if item.get('thumbnail_url'):
199
+ story["thumbnail"] = item.get('thumbnail_url')
200
 
201
+ stories.append(story)
202
+
203
+ result = {
204
+ "data": stories,
205
+ "count": len(stories),
206
+ "username": username,
207
+ "fetched_at": datetime.now().isoformat()
208
+ }
209
+
210
+ return result
211
+
212
  except Exception as e:
213
+ logger.error(f"Error fetching stories from alternative: {str(e)}")
214
  raise
215
 
216
+ def fetch_stories_from_third_source(username):
217
+ """Try a third source for stories"""
218
+ url = f"https://instasupersave.com/api/ig/stories/{username}"
 
 
 
 
 
 
219
 
220
  try:
221
+ session = requests.Session()
 
 
 
 
 
 
222
  session.headers.update({
223
+ 'User-Agent': random.choice(USER_AGENTS),
224
+ 'Accept': 'application/json',
225
+ 'Referer': f'https://instasupersave.com/instagram-stories/{username}/',
226
+ 'Origin': 'https://instasupersave.com'
 
227
  })
228
 
229
+ # Add random delay
230
+ time.sleep(get_random_delay())
 
 
 
 
 
 
 
 
 
 
231
 
232
+ response = session.get(url)
233
+ response.raise_for_status()
234
+ data = response.json()
 
 
 
 
 
 
 
 
 
235
 
236
+ if not data or not data.get('result') or not data['result'].get('stories'):
237
+ logger.warning(f"No stories found for {username} on third source")
238
+ raise Exception("No stories found")
239
 
240
  # Process the stories
241
  stories = []
242
+ for item in data['result'].get('stories', []):
 
 
243
  story = {
244
  "id": item.get('id', ''),
245
+ "type": "video" if item.get('is_video', False) else "image",
246
+ "timestamp": datetime.fromtimestamp(item.get('taken_at', 0)).isoformat(),
247
+ "url": item.get('source', '')
248
  }
249
 
250
+ if item.get('thumbnail'):
251
+ story["thumbnail"] = item.get('thumbnail')
252
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  stories.append(story)
254
 
255
  result = {
256
  "data": stories,
257
  "count": len(stories),
258
  "username": username,
259
+ "fetched_at": datetime.now().isoformat(),
260
+ "source": "third"
261
  }
262
 
263
  return result
264
+
265
  except Exception as e:
266
+ logger.error(f"Error fetching stories from third source: {str(e)}")
 
 
267
  raise
268
 
269
  @app.get("/stories/{username}")
 
289
  detail="Rate limit exceeded for this username. Please try again later."
290
  )
291
 
292
+ # Try multiple sources in sequence
293
+ result = None
294
+ sources_tried = 0
295
+ error_messages = []
296
+
297
+ # Try first source
298
+ try:
299
+ sources_tried += 1
300
+ result = fetch_stories_from_web(username)
301
+ except Exception as e:
302
+ error_messages.append(f"Source 1: {str(e)}")
303
+ # Let's try the second source
304
+ try:
305
+ sources_tried += 1
306
+ time.sleep(get_random_delay()) # Add delay between attempts
307
+ result = fetch_stories_from_alternative(username)
308
+ except Exception as e2:
309
+ error_messages.append(f"Source 2: {str(e2)}")
310
+ # Let's try the third source
311
+ try:
312
+ sources_tried += 1
313
+ time.sleep(get_random_delay())
314
+ result = fetch_stories_from_third_source(username)
315
+ except Exception as e3:
316
+ error_messages.append(f"Source 3: {str(e3)}")
317
+
318
+ if result:
319
+ # Cache the successful result
320
+ save_to_cache(username, result)
321
+ result["sources_tried"] = sources_tried
322
+ return result
323
+
324
+ # All sources failed, check cache
325
+ cached_result = get_cached_stories(username)
326
+ if cached_result:
327
+ logger.info(f"All sources failed, returning cached result")
328
+ return {
329
+ **cached_result,
330
+ "from_cache": True,
331
+ "sources_tried": sources_tried,
332
+ "errors": error_messages
333
+ }
334
 
335
+ # No result from any source and no cache
336
+ raise HTTPException(
337
+ status_code=status.HTTP_404_NOT_FOUND,
338
+ detail=f"Stories not found after trying {sources_tried} sources"
339
+ )
340
 
341
+ except HTTPException:
342
+ # Re-raise HTTP exceptions directly
343
+ raise
344
  except Exception as e:
345
  error_message = str(e)
346
  logger.error(f"Error getting stories: {error_message}")
 
352
  return {**cached_result, "from_cache": True, "error_occurred": True}
353
 
354
  # Handle specific errors
355
+ if 'rate' in error_message.lower() or 'limit' in error_message.lower():
356
  raise HTTPException(
357
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
358
+ detail="Rate limit exceeded. Please try again later."
359
  )
360
+ elif 'no stories' in error_message.lower():
361
  raise HTTPException(
362
  status_code=status.HTTP_404_NOT_FOUND,
363
+ detail="No stories found for this user"
 
 
 
 
 
364
  )
365
  else:
366
  raise HTTPException(
 
371
  @app.get("/download/{url:path}")
372
  async def download_media(url: str):
373
  """Download and proxy media content"""
374
+ logger.info(f"Download request for media")
375
 
376
  try:
377
  # Validate URL
378
+ if not url.startswith(("https://", "http://")):
379
  raise HTTPException(
380
  status_code=status.HTTP_400_BAD_REQUEST,
381
  detail="Invalid URL format"
382
  )
383
 
384
  # Configure request
385
+ session = requests.Session()
386
  headers = {
387
  "User-Agent": random.choice(USER_AGENTS),
388
  "Referer": "https://www.instagram.com/",