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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -107
app.py CHANGED
@@ -7,7 +7,7 @@ import logging
7
  import random
8
  import json
9
  from datetime import datetime, timedelta
10
- import re
11
  from urllib.parse import quote
12
 
13
  # Configure logging
@@ -28,39 +28,37 @@ 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()
@@ -111,15 +109,15 @@ def should_rate_limit_request(username):
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:
@@ -133,87 +131,97 @@ def get_demo_stories(username):
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):
@@ -243,13 +251,13 @@ async def get_stories(username: str, cached: bool = False, demo: bool = False):
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)
@@ -260,7 +268,7 @@ async def get_stories(username: str, cached: bool = False, demo: bool = False):
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)
@@ -274,7 +282,6 @@ async def get_stories(username: str, cached: bool = False, demo: bool = False):
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}")
@@ -328,6 +335,23 @@ async def download_media(url: str):
328
  detail="Failed to download media"
329
  )
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  # Load cache from disk at startup
332
  @app.on_event("startup")
333
  def load_cache_from_disk():
@@ -359,6 +383,8 @@ def load_cache_from_disk():
359
  async def health_check():
360
  return {
361
  "status": "ok",
 
 
362
  "timestamp": datetime.now().isoformat(),
363
  "cache_size": len(STORY_CACHE),
364
  }
 
7
  import random
8
  import json
9
  from datetime import datetime, timedelta
10
+ import hashlib
11
  from urllib.parse import quote
12
 
13
  # Configure logging
 
28
  CACHE_EXPIRY = {}
29
  LAST_REQUEST = {}
30
 
31
+ # User agents
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
  ]
37
 
38
+ # More diverse demo content for better user experience
 
 
 
 
 
 
 
 
 
39
  DEMO_STORIES = {
40
+ "type1": [
41
+ {"id": "nature1", "type": "image", "url": "https://images.unsplash.com/photo-1682687980961-78fa83781450"},
42
+ {"id": "nature2", "type": "image", "url": "https://images.unsplash.com/photo-1709921233257-4d6fad0af5c8"}
43
+ ],
44
+ "type2": [
45
+ {"id": "city1", "type": "image", "url": "https://images.unsplash.com/photo-1710319367362-089a828a423c"},
46
+ {"id": "city2", "type": "video", "url": "https://assets.mixkit.co/videos/preview/mixkit-aerial-view-of-city-traffic-at-night-11-large.mp4"}
47
+ ],
48
+ "type3": [
49
+ {"id": "food1", "type": "image", "url": "https://images.unsplash.com/photo-1710170883104-2e9ac8709bb7"},
50
+ {"id": "food2", "type": "image", "url": "https://images.unsplash.com/photo-1683009427500-71a13c0dce20"}
51
  ],
52
+ "type4": [
53
+ {"id": "travel1", "type": "image", "url": "https://images.unsplash.com/photo-1682687982501-1e58ab814714"},
54
+ {"id": "travel2", "type": "video", "url": "https://assets.mixkit.co/videos/preview/mixkit-going-down-a-curved-highway-through-a-mountain-range-41576-large.mp4"}
55
  ],
56
+ "type5": [
57
+ {"id": "people1", "type": "image", "url": "https://images.unsplash.com/photo-1710161380135-9b617884d65f"},
58
+ {"id": "people2", "type": "video", "url": "https://assets.mixkit.co/videos/preview/mixkit-man-dancing-under-changing-lights-1240-large.mp4"}
59
+ ]
60
  }
61
 
 
 
 
62
  def get_cache_key(username):
63
  """Generate a cache key from username"""
64
  return username.lower()
 
109
  return False
110
 
111
  def get_demo_stories(username):
112
+ """Return demo stories for a username with deterministic but varied selection"""
113
  logger.info(f"Using demo stories for {username}")
114
 
115
+ # Use hash of username to deterministically select a demo type for each username
116
+ # This ensures the same username always gets the same demo content
117
+ hash_value = int(hashlib.md5(username.lower().encode()).hexdigest(), 16)
118
+ demo_type = f"type{(hash_value % 5) + 1}" # Get a number between 1-5
119
+
120
+ stories = DEMO_STORIES[demo_type]
121
 
122
  # Add timestamps to make them seem fresh
123
  for story in stories:
 
131
  "count": len(stories),
132
  "username": username,
133
  "demo": True, # Mark as demo content
134
+ "note": "Using placeholder content - Instagram API access is currently restricted",
135
  "fetched_at": datetime.now().isoformat()
136
  }
137
 
138
  return result
139
 
140
+ def try_get_real_stories(username):
141
+ """Attempt to get real stories using multiple methods"""
142
+ # Try method 1: Direct API (very likely to fail due to restrictions)
143
  try:
144
+ logger.info(f"Attempting direct API access for {username}")
145
+ url = f"https://i.instagram.com/api/v1/feed/user/{username}/story/"
146
  headers = {
147
+ 'User-Agent': 'Instagram 219.0.0.12.117 Android',
148
+ 'Accept-Language': 'en-US',
 
 
 
 
 
 
 
 
149
  }
150
+ response = requests.get(url, headers=headers, timeout=5)
151
 
152
+ if response.status_code == 200:
153
+ data = response.json()
154
+ if 'reel' in data and 'items' in data['reel'] and data['reel']['items']:
155
+ logger.info(f"Successfully got stories via direct API for {username}")
156
+ # Process items and return
157
+ stories = []
158
+ for item in data['reel']['items']:
159
+ story = {
160
+ "id": item.get('pk', ''),
161
+ "type": "video" if item.get('media_type') == 2 else "image",
162
+ "timestamp": datetime.fromtimestamp(item.get('taken_at')).isoformat()
163
+ }
164
+
165
+ if story["type"] == "video" and 'video_versions' in item and item['video_versions']:
166
+ story["url"] = item['video_versions'][0]['url']
167
+ elif 'image_versions2' in item and 'candidates' in item['image_versions2'] and item['image_versions2']['candidates']:
168
+ story["url"] = item['image_versions2']['candidates'][0]['url']
169
+ else:
170
+ continue # Skip if no URL
171
+
172
+ stories.append(story)
173
+
174
+ if stories:
175
+ return {
176
+ "data": stories,
177
+ "count": len(stories),
178
+ "username": username,
179
+ "source": "instagram_api",
180
+ "fetched_at": datetime.now().isoformat()
181
+ }
182
+ except Exception as e:
183
+ logger.warning(f"Direct API access failed: {str(e)}")
184
+
185
+ # Try method 2: Unofficial service (if available)
186
+ try:
187
+ logger.info(f"Attempting unofficial service for {username}")
188
+ url = f"https://instagram-stories1.p.rapidapi.com/v1/get_stories?username={username}"
189
+ headers = {
190
+ "X-RapidAPI-Key": os.getenv('RAPIDAPI_KEY', ''), # Set this in your environment if available
191
+ "X-RapidAPI-Host": "instagram-stories1.p.rapidapi.com"
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
 
194
+ if headers["X-RapidAPI-Key"]: # Only try if API key is set
195
+ response = requests.get(url, headers=headers, timeout=10)
196
+
197
+ if response.status_code == 200:
198
+ data = response.json()
199
+ if isinstance(data, dict) and 'stories' in data and data['stories']:
200
+ logger.info(f"Successfully got stories via unofficial service for {username}")
201
+ stories = []
202
+
203
+ for item in data['stories']:
204
+ story = {
205
+ "id": item.get('id', str(hash(item.get('media_url', '')))),
206
+ "type": item.get('media_type', 'image'),
207
+ "url": item.get('media_url', ''),
208
+ "timestamp": item.get('timestamp', datetime.now().isoformat())
209
+ }
210
+ stories.append(story)
211
+
212
+ if stories:
213
+ return {
214
+ "data": stories,
215
+ "count": len(stories),
216
+ "username": username,
217
+ "source": "unofficial_api",
218
+ "fetched_at": datetime.now().isoformat()
219
+ }
220
  except Exception as e:
221
+ logger.warning(f"Unofficial service failed: {str(e)}")
222
+
223
+ # All methods failed
224
+ return None
225
 
226
  @app.get("/stories/{username}")
227
  async def get_stories(username: str, cached: bool = False, demo: bool = False):
 
251
  save_to_cache(username, demo_stories, minutes=5) # Short cache time for demo content
252
  return {**demo_stories, "rate_limited": True}
253
 
254
+ # Try to get real stories
255
+ real_stories = try_get_real_stories(username)
256
 
257
+ if real_stories:
258
  # Cache the successful result
259
+ save_to_cache(username, real_stories)
260
+ return real_stories
261
 
262
  # If no result, check cache
263
  cached_result = get_cached_stories(username)
 
268
  # No result and no cache, use demo content
269
  demo_stories = get_demo_stories(username)
270
  save_to_cache(username, demo_stories, minutes=30)
271
+ return demo_stories
272
 
273
  except Exception as e:
274
  error_message = str(e)
 
282
 
283
  # No cache available, use demo content
284
  demo_stories = get_demo_stories(username)
 
285
  return {**demo_stories, "error_occurred": True}
286
 
287
  @app.get("/download/{url:path}")
 
335
  detail="Failed to download media"
336
  )
337
 
338
+ # Add an endpoint to explain what's happening
339
+ @app.get("/")
340
+ async def root():
341
+ return {
342
+ "message": "Instagram Stories API",
343
+ "status": "Running with demo mode",
344
+ "note": "Due to Instagram's API restrictions, this service currently provides placeholder content.",
345
+ "endpoints": {
346
+ "get_stories": "/stories/{username}",
347
+ "get_stories_from_cache": "/stories/{username}?cached=true",
348
+ "get_demo_stories": "/stories/{username}?demo=true",
349
+ "download_media": "/download/{url}",
350
+ "health_check": "/health"
351
+ },
352
+ "timestamp": datetime.now().isoformat()
353
+ }
354
+
355
  # Load cache from disk at startup
356
  @app.on_event("startup")
357
  def load_cache_from_disk():
 
383
  async def health_check():
384
  return {
385
  "status": "ok",
386
+ "mode": "demo",
387
+ "note": "Instagram access restricted - using placeholder content",
388
  "timestamp": datetime.now().isoformat(),
389
  "cache_size": len(STORY_CACHE),
390
  }