Chrunos commited on
Commit
d0e1f2a
·
verified ·
1 Parent(s): eaba9ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -182
app.py CHANGED
@@ -7,8 +7,8 @@ import logging
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,8 +20,13 @@ logger = logging.getLogger(__name__)
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,11 +34,16 @@ STORY_CACHE = {}
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,176 +104,242 @@ def set_rate_limit(username, minutes=15):
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,58 +365,14 @@ async def get_stories(username: str, cached: bool = False):
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,15 +384,20 @@ async def get_stories(username: str, cached: bool = False):
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,18 +408,18 @@ async def get_stories(username: str, cached: bool = False):
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/",
 
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
 
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
  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
  }
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
  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
  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
  @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/",