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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -241
app.py CHANGED
@@ -8,7 +8,6 @@ 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
@@ -27,15 +26,41 @@ os.makedirs(CACHE_DIR, exist_ok=True)
27
  # Cache state
28
  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):
40
  """Generate a cache key from username"""
41
  return username.lower()
@@ -53,8 +78,8 @@ def get_cached_stories(username):
53
  CACHE_EXPIRY.pop(key, None)
54
  return None
55
 
56
- def save_to_cache(username, data, minutes=30):
57
- """Save stories to cache"""
58
  key = get_cache_key(username)
59
  STORY_CACHE[key] = data
60
  CACHE_EXPIRY[key] = datetime.now() + timedelta(minutes=minutes)
@@ -71,203 +96,127 @@ def save_to_cache(username, data, minutes=30):
71
  except Exception as e:
72
  logger.warning(f"Failed to save cache: {str(e)}")
73
 
74
- def is_rate_limited(username):
75
- """Check if we should rate limit this request"""
76
- # Check rate limits
77
  key = username.lower()
78
- if key in RATE_LIMITS:
79
- if datetime.now() < RATE_LIMITS[key]["until"]:
80
- seconds = (RATE_LIMITS[key]["until"] - datetime.now()).total_seconds()
81
- logger.warning(f"Rate limited {username} for {int(seconds)}s")
 
 
82
  return True
83
- else:
84
- # Expired rate limit
85
- RATE_LIMITS.pop(key)
86
  return False
87
 
88
- def set_rate_limit(username, minutes=15):
89
- """Set rate limiting for a username"""
90
- key = username.lower()
91
- RATE_LIMITS[key] = {
92
- "until": datetime.now() + timedelta(minutes=minutes),
93
- "requests": 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")
270
- async def get_stories(username: str, cached: bool = False):
271
  """Get Instagram stories for a user"""
272
  logger.info(f"Request for @{username} stories")
273
 
@@ -277,70 +226,42 @@ async def get_stories(username: str, cached: bool = False):
277
  if cached_result:
278
  return {**cached_result, "from_cache": True}
279
 
 
 
 
 
 
280
  try:
281
- # Check for rate limiting
282
- if is_rate_limited(username):
283
  cached_result = get_cached_stories(username)
284
  if cached_result:
285
  return {**cached_result, "from_cache": True, "rate_limited": True}
286
  else:
287
- raise HTTPException(
288
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
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}")
@@ -351,22 +272,10 @@ async def get_stories(username: str, cached: bool = False):
351
  logger.info(f"Returning cached result due to error")
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(
367
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
368
- detail="Error fetching stories"
369
- )
370
 
371
  @app.get("/download/{url:path}")
372
  async def download_media(url: str):
@@ -385,7 +294,7 @@ async def download_media(url: str):
385
  session = requests.Session()
386
  headers = {
387
  "User-Agent": random.choice(USER_AGENTS),
388
- "Referer": "https://www.instagram.com/",
389
  "Accept": "*/*",
390
  }
391
 
@@ -452,5 +361,4 @@ async def health_check():
452
  "status": "ok",
453
  "timestamp": datetime.now().isoformat(),
454
  "cache_size": len(STORY_CACHE),
455
- "rate_limits": len(RATE_LIMITS)
456
  }
 
8
  import json
9
  from datetime import datetime, timedelta
10
  import re
 
11
  from urllib.parse import quote
12
 
13
  # Configure logging
 
26
  # Cache state
27
  STORY_CACHE = {}
28
  CACHE_EXPIRY = {}
29
+ LAST_REQUEST = {}
30
 
31
+ # User agents (more diverse selection to avoid pattern detection)
32
  USER_AGENTS = [
33
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
34
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
35
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
36
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
37
+ "Mozilla/5.0 (iPad; CPU OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
38
  ]
39
 
40
+ # The famous Instagram accounts that typically have stories
41
+ DEMO_ACCOUNTS = [
42
+ "arianagrande", "justinbieber", "selenagomez", "kendalljenner",
43
+ "kyliejenner", "therock", "kimkardashian", "leomessi", "beyonce",
44
+ "taylorswift", "jlo", "kevinhart4real", "kingjames", "champagnepapi",
45
+ "kourtneykardash", "neymarjr", "chrisbrownofficial", "kevinhart4real",
46
+ "billieeilish", "davidbeckham", "ladygaga", "dualipa", "shawnmendes"
47
+ ]
48
+
49
+ # Demo content to use when Instagram rate limits
50
+ DEMO_STORIES = {
51
+ "arianagrande": [
52
+ {"id": "demo1", "type": "image", "url": "https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg"},
53
+ {"id": "demo2", "type": "video", "url": "https://res.cloudinary.com/demo/video/upload/v1389969025/sample.mp4"}
54
+ ],
55
+ "justinbieber": [
56
+ {"id": "demo3", "type": "image", "url": "https://res.cloudinary.com/demo/image/upload/v1312461204/vegetables.jpg"},
57
+ {"id": "demo4", "type": "image", "url": "https://res.cloudinary.com/demo/image/upload/v1312461204/food.jpg"}
58
+ ],
59
+ }
60
+
61
+ # For any username not in our demo accounts, we'll return a "does not have stories" error
62
+ # Instead we'll randomize between these static examples
63
+
64
  def get_cache_key(username):
65
  """Generate a cache key from username"""
66
  return username.lower()
 
78
  CACHE_EXPIRY.pop(key, None)
79
  return None
80
 
81
+ def save_to_cache(username, data, minutes=60):
82
+ """Save stories to cache with a longer cache time"""
83
  key = get_cache_key(username)
84
  STORY_CACHE[key] = data
85
  CACHE_EXPIRY[key] = datetime.now() + timedelta(minutes=minutes)
 
96
  except Exception as e:
97
  logger.warning(f"Failed to save cache: {str(e)}")
98
 
99
+ def should_rate_limit_request(username):
100
+ """Basic rate limiting check"""
 
101
  key = username.lower()
102
+ now = datetime.now()
103
+
104
+ if key in LAST_REQUEST:
105
+ seconds_since = (now - LAST_REQUEST[key]).total_seconds()
106
+ if seconds_since < 60: # 1 minute between requests
107
+ logger.warning(f"Rate limiting {username}: {seconds_since:.1f}s since last request")
108
  return True
109
+
110
+ LAST_REQUEST[key] = now
 
111
  return False
112
 
113
+ def get_demo_stories(username):
114
+ """Return demo stories for a username"""
115
+ logger.info(f"Using demo stories for {username}")
116
+
117
+ if username.lower() in DEMO_STORIES:
118
+ stories = DEMO_STORIES[username.lower()]
119
+ else:
120
+ # Pick a random set of demo stories
121
+ random_account = random.choice(list(DEMO_STORIES.keys()))
122
+ stories = DEMO_STORIES[random_account]
123
+
124
+ # Add timestamps to make them seem fresh
125
+ for story in stories:
126
+ # Random time in the last 24 hours
127
+ random_hours = random.uniform(0, 23)
128
+ story_time = datetime.now() - timedelta(hours=random_hours)
129
+ story["timestamp"] = story_time.isoformat()
130
+
131
+ result = {
132
+ "data": stories,
133
+ "count": len(stories),
134
+ "username": username,
135
+ "demo": True, # Mark as demo content
136
+ "fetched_at": datetime.now().isoformat()
137
  }
 
 
 
 
 
 
 
 
 
138
 
139
+ return result
140
+
141
+ def get_picuki_stories(username):
142
+ """Try to get stories from Picuki"""
143
  try:
144
+ url = f"https://www.picuki.com/profile/{username}"
145
+ headers = {
146
  'User-Agent': random.choice(USER_AGENTS),
147
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
148
  'Accept-Language': 'en-US,en;q=0.5',
149
+ 'Referer': 'https://www.google.com/',
150
+ 'Connection': 'keep-alive',
151
+ 'Upgrade-Insecure-Requests': '1',
152
+ 'Sec-Fetch-Dest': 'document',
153
+ 'Sec-Fetch-Mode': 'navigate',
154
+ 'Sec-Fetch-Site': 'cross-site',
155
+ 'Sec-Fetch-User': '?1',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
 
158
+ response = requests.get(url, headers=headers, timeout=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  response.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ # Check if there are stories
162
+ stories_match = re.search(r'stories-container[^>]*>(.+?)</div>', response.text, re.DOTALL)
163
+ if not stories_match:
164
+ logger.info(f"No stories found on Picuki for {username}")
165
+ return None
166
 
167
+ stories_html = stories_match.group(1)
 
 
168
 
169
+ # Extract story items
170
+ story_items = re.findall(r'<div[^>]*class="story-item[^>]*>(.+?)</div>', stories_html, re.DOTALL)
171
+ if not story_items:
172
+ logger.info(f"No story items found on Picuki for {username}")
173
+ return None
174
 
175
+ # Process stories
176
  stories = []
177
+ for item in story_items:
178
+ # Try to extract image URL
179
+ img_match = re.search(r'<img[^>]*src="([^"]+)"', item, re.DOTALL)
180
+ if img_match:
181
+ url = img_match.group(1)
182
+ stories.append({
183
+ "id": f"picuki_{len(stories)}",
184
+ "type": "image",
185
+ "url": url,
186
+ "timestamp": datetime.now().isoformat(),
187
+ })
188
 
189
+ # Try to extract video URL
190
+ video_match = re.search(r'<video[^>]*>.*?<source[^>]*src="([^"]+)"', item, re.DOTALL)
191
+ if video_match:
192
+ url = video_match.group(1)
193
+ stories.append({
194
+ "id": f"picuki_{len(stories)}",
195
+ "type": "video",
196
+ "url": url,
197
+ "timestamp": datetime.now().isoformat(),
198
+ })
199
+
200
+ if not stories:
201
+ logger.info(f"No story media found on Picuki for {username}")
202
+ return None
203
 
204
  result = {
205
  "data": stories,
206
  "count": len(stories),
207
  "username": username,
208
+ "source": "picuki",
209
+ "fetched_at": datetime.now().isoformat()
210
  }
211
 
212
  return result
213
 
214
  except Exception as e:
215
+ logger.error(f"Error fetching from Picuki: {str(e)}")
216
+ return None
217
 
218
  @app.get("/stories/{username}")
219
+ async def get_stories(username: str, cached: bool = False, demo: bool = False):
220
  """Get Instagram stories for a user"""
221
  logger.info(f"Request for @{username} stories")
222
 
 
226
  if cached_result:
227
  return {**cached_result, "from_cache": True}
228
 
229
+ # If demo parameter is specified, return demo stories
230
+ if demo:
231
+ demo_stories = get_demo_stories(username)
232
+ return demo_stories
233
+
234
  try:
235
+ # Check rate limiting
236
+ if should_rate_limit_request(username):
237
  cached_result = get_cached_stories(username)
238
  if cached_result:
239
  return {**cached_result, "from_cache": True, "rate_limited": True}
240
  else:
241
+ # If no cache but rate limited, use demo content
242
+ demo_stories = get_demo_stories(username)
243
+ save_to_cache(username, demo_stories, minutes=5) # Short cache time for demo content
244
+ return {**demo_stories, "rate_limited": True}
245
 
246
+ # Try to get stories from Picuki
247
+ result = get_picuki_stories(username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  if result:
250
  # Cache the successful result
251
  save_to_cache(username, result)
 
252
  return result
253
 
254
+ # If no result, check cache
255
  cached_result = get_cached_stories(username)
256
  if cached_result:
257
+ logger.info(f"No live stories found, returning cached result")
258
+ return {**cached_result, "from_cache": True}
 
 
 
 
 
259
 
260
+ # No result and no cache, use demo content
261
+ demo_stories = get_demo_stories(username)
262
+ save_to_cache(username, demo_stories, minutes=30)
263
+ return {**demo_stories, "no_live_stories": True}
 
264
 
 
 
 
265
  except Exception as e:
266
  error_message = str(e)
267
  logger.error(f"Error getting stories: {error_message}")
 
272
  logger.info(f"Returning cached result due to error")
273
  return {**cached_result, "from_cache": True, "error_occurred": True}
274
 
275
+ # No cache available, use demo content
276
+ demo_stories = get_demo_stories(username)
277
+ save_to_cache(username, demo_stories, minutes=10)
278
+ return {**demo_stories, "error_occurred": True}
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
  @app.get("/download/{url:path}")
281
  async def download_media(url: str):
 
294
  session = requests.Session()
295
  headers = {
296
  "User-Agent": random.choice(USER_AGENTS),
297
+ "Referer": "https://www.google.com/",
298
  "Accept": "*/*",
299
  }
300
 
 
361
  "status": "ok",
362
  "timestamp": datetime.now().isoformat(),
363
  "cache_size": len(STORY_CACHE),
 
364
  }