Chrunos commited on
Commit
226ea29
·
verified ·
1 Parent(s): 633133c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +350 -479
app.py CHANGED
@@ -1,7 +1,5 @@
1
- from fastapi import FastAPI, HTTPException, status, BackgroundTasks, Request, Form, Cookie, Depends
2
- from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse, RedirectResponse
3
- from fastapi.templating import Jinja2Templates
4
- from fastapi.staticfiles import StaticFiles
5
  import requests
6
  import os
7
  import time
@@ -10,10 +8,7 @@ import random
10
  import json
11
  from datetime import datetime, timedelta
12
  import re
13
- from urllib.parse import quote
14
  from typing import Optional
15
- import secrets
16
- from pathlib import Path
17
 
18
  # Configure logging
19
  logging.basicConfig(
@@ -24,178 +19,158 @@ logger = logging.getLogger(__name__)
24
 
25
  app = FastAPI(title="Instagram Stories API")
26
 
27
- # Create templates and static directories
28
- templates_dir = Path("templates")
29
- templates_dir.mkdir(exist_ok=True)
30
- static_dir = Path("static")
31
- static_dir.mkdir(exist_ok=True)
32
 
33
- # Create template files
34
- login_template = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  <!DOCTYPE html>
36
  <html>
37
  <head>
38
- <title>Instagram Login</title>
39
  <style>
40
- body {
41
- font-family: Arial, sans-serif;
42
- max-width: 500px;
43
- margin: 0 auto;
44
- padding: 20px;
45
- }
46
- .container {
47
- background-color: #fff;
48
- border-radius: 8px;
49
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
50
- padding: 25px;
51
- }
52
- h1 {
53
- color: #262626;
54
- text-align: center;
55
- }
56
- .form-group {
57
- margin-bottom: 15px;
58
- }
59
- label {
60
- display: block;
61
- margin-bottom: 5px;
62
- font-weight: bold;
63
- }
64
- input[type="text"], input[type="password"] {
65
- width: 100%;
66
- padding: 10px;
67
- border: 1px solid #dbdbdb;
68
- border-radius: 3px;
69
- box-sizing: border-box;
70
- }
71
- .btn {
72
- background-color: #0095f6;
73
- border: none;
74
- color: white;
75
- padding: 10px 15px;
76
- border-radius: 4px;
77
- cursor: pointer;
78
- font-weight: bold;
79
- width: 100%;
80
- }
81
- .btn:hover {
82
- background-color: #0086e0;
83
- }
84
- .status {
85
- margin-top: 20px;
86
- padding: 10px;
87
- border-radius: 4px;
88
- }
89
- .success {
90
- background-color: #e8f5e9;
91
- color: #388e3c;
92
- }
93
- .error {
94
- background-color: #ffebee;
95
- color: #d32f2f;
96
- }
97
  </style>
98
  </head>
99
  <body>
100
  <div class="container">
101
- <h1>Instagram Login</h1>
102
- <p>Enter your Instagram credentials to access the API. Your credentials are only used to establish a session and are not stored on the server.</p>
103
 
104
- {% if message %}
105
- <div class="status {% if success %}success{% else %}error{% endif %}">
106
- {{ message }}
107
- </div>
108
- {% endif %}
109
 
110
- <form method="post" action="/login">
111
- <div class="form-group">
112
- <label for="username">Username</label>
113
- <input type="text" id="username" name="username" required>
 
 
 
 
 
 
 
 
114
  </div>
115
- <div class="form-group">
116
- <label for="password">Password</label>
117
- <input type="password" id="password" name="password" required>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </div>
119
- <button type="submit" class="btn">Log In</button>
120
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </body>
123
  </html>
124
  """
125
 
126
- status_template = """
 
127
  <!DOCTYPE html>
128
  <html>
129
  <head>
130
- <title>Instagram API Status</title>
131
  <style>
132
- body {
133
- font-family: Arial, sans-serif;
134
- max-width: 700px;
135
- margin: 0 auto;
136
- padding: 20px;
137
- }
138
- .container {
139
- background-color: #fff;
140
- border-radius: 8px;
141
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
142
- padding: 25px;
143
- }
144
- h1, h2 {
145
- color: #262626;
146
- }
147
- h1 {
148
- text-align: center;
149
- }
150
- .status-badge {
151
- display: inline-block;
152
- padding: 5px 10px;
153
- border-radius: 4px;
154
- font-weight: bold;
155
- margin-left: 10px;
156
- }
157
- .connected {
158
- background-color: #e8f5e9;
159
- color: #388e3c;
160
- }
161
- .disconnected {
162
- background-color: #ffebee;
163
- color: #d32f2f;
164
- }
165
- .info-box {
166
- background-color: #f5f5f5;
167
- border-radius: 4px;
168
- padding: 15px;
169
- margin: 15px 0;
170
- }
171
- .btn {
172
- background-color: #0095f6;
173
- border: none;
174
- color: white;
175
- padding: 10px 15px;
176
- border-radius: 4px;
177
- text-decoration: none;
178
- display: inline-block;
179
- margin-right: 10px;
180
- margin-top: 10px;
181
- font-weight: bold;
182
- }
183
- .btn:hover {
184
- background-color: #0086e0;
185
- }
186
- table {
187
- width: 100%;
188
- border-collapse: collapse;
189
- margin-top: 15px;
190
- }
191
- th, td {
192
- text-align: left;
193
- padding: 8px;
194
- border-bottom: 1px solid #ddd;
195
- }
196
- th {
197
- background-color: #f8f9fa;
198
- }
199
  </style>
200
  </head>
201
  <body>
@@ -203,116 +178,54 @@ status_template = """
203
  <h1>Instagram API Status</h1>
204
 
205
  <h2>Connection Status
206
- {% if session_valid %}
207
- <span class="status-badge connected">Connected</span>
208
- {% else %}
209
- <span class="status-badge disconnected">Disconnected</span>
210
- {% endif %}
211
  </h2>
212
 
213
- <div class="info-box">
214
- {% if session_valid %}
215
- <p>✅ You are currently logged in as <strong>{{ username }}</strong></p>
216
- <p>Session will expire: {{ session_expires }}</p>
217
- {% else %}
218
- <p>❌ No active Instagram session</p>
219
- <p>Please login to use the API with full functionality</p>
220
- {% endif %}
221
  </div>
222
 
223
- <a href="/login" class="btn">{% if session_valid %}Re-Login{% else %}Login{% endif %}</a>
224
- {% if session_valid %}
225
- <a href="/logout" class="btn" style="background-color: #f44336;">Logout</a>
226
- {% endif %}
227
 
228
  <h2>API Endpoints</h2>
229
  <table>
230
- <tr>
231
- <th>Endpoint</th>
232
- <th>Description</th>
233
- </tr>
234
- <tr>
235
- <td><code>/stories/{username}</code></td>
236
- <td>Get stories for a user</td>
237
- </tr>
238
- <tr>
239
- <td><code>/download/{url}</code></td>
240
- <td>Download media from a URL</td>
241
- </tr>
242
- <tr>
243
- <td><code>/status</code></td>
244
- <td>View API status and session info</td>
245
- </tr>
246
  </table>
247
 
248
- {% if cache_info %}
249
- <h2>Cache Status</h2>
250
- <div class="info-box">
251
- <p>Cached users: {{ cache_info.count }}</p>
252
- <p>Total items: {{ cache_info.items }}</p>
253
  </div>
254
- {% endif %}
255
  </div>
256
  </body>
257
  </html>
258
  """
259
 
260
- # Write templates to files
261
- with open(templates_dir / "login.html", "w") as f:
262
- f.write(login_template)
263
-
264
- with open(templates_dir / "status.html", "w") as f:
265
- f.write(status_template)
266
-
267
- # Set up templates and static files
268
- templates = Jinja2Templates(directory=str(templates_dir))
269
- app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
270
-
271
- # Configuration
272
- CACHE_DIR = "/tmp/instagram_cache"
273
- COOKIES_FILE = "/tmp/instagram_cookies.json"
274
- SESSION_KEY = secrets.token_hex(16) # For securing session
275
- os.makedirs(CACHE_DIR, exist_ok=True)
276
-
277
- # Session and cache state
278
- IG_SESSION = None
279
- IG_SESSION_USERNAME = None
280
- IG_SESSION_EXPIRY = None
281
- STORY_CACHE = {}
282
- CACHE_EXPIRY = {}
283
- LAST_REQUEST = {}
284
-
285
- # User agents
286
- USER_AGENTS = [
287
- "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",
288
- "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",
289
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
290
- ]
291
-
292
  def get_cache_key(username):
293
- """Generate a cache key from username"""
294
  return username.lower()
295
 
296
  def get_cached_stories(username):
297
- """Get stories from cache if available"""
298
  key = get_cache_key(username)
299
  if key in STORY_CACHE and key in CACHE_EXPIRY:
300
  if datetime.now() < CACHE_EXPIRY[key]:
301
  logger.info(f"Cache hit for {username}")
302
  return STORY_CACHE[key]
303
  else:
304
- # Clean expired cache
305
  STORY_CACHE.pop(key, None)
306
  CACHE_EXPIRY.pop(key, None)
307
  return None
308
 
309
  def save_to_cache(username, data, minutes=30):
310
- """Save stories to cache"""
311
  key = get_cache_key(username)
312
  STORY_CACHE[key] = data
313
  CACHE_EXPIRY[key] = datetime.now() + timedelta(minutes=minutes)
314
 
315
- # Save to disk
316
  try:
317
  cache_file = os.path.join(CACHE_DIR, f"{key}.json")
318
  with open(cache_file, 'w') as f:
@@ -325,13 +238,12 @@ def save_to_cache(username, data, minutes=30):
325
  logger.warning(f"Failed to save cache: {str(e)}")
326
 
327
  def should_rate_limit_request(username):
328
- """Basic rate limiting check"""
329
  key = username.lower()
330
  now = datetime.now()
331
 
332
  if key in LAST_REQUEST:
333
  seconds_since = (now - LAST_REQUEST[key]).total_seconds()
334
- if seconds_since < 60: # 1 minute between requests
335
  logger.warning(f"Rate limiting {username}: {seconds_since:.1f}s since last request")
336
  return True
337
 
@@ -339,12 +251,9 @@ def should_rate_limit_request(username):
339
  return False
340
 
341
  def get_instagram_session():
342
- """Get current Instagram session"""
343
  global IG_SESSION, IG_SESSION_EXPIRY
344
 
345
- # Check if session is still valid
346
  if IG_SESSION is None or IG_SESSION_EXPIRY is None or datetime.now() > IG_SESSION_EXPIRY:
347
- # Try to load from file
348
  if os.path.exists(COOKIES_FILE):
349
  try:
350
  with open(COOKIES_FILE, 'r') as f:
@@ -353,7 +262,6 @@ def get_instagram_session():
353
  if 'expires' in data and datetime.fromisoformat(data['expires']) > datetime.now():
354
  IG_SESSION = requests.Session()
355
 
356
- # Set cookies
357
  for cookie in data['cookies']:
358
  IG_SESSION.cookies.set(
359
  cookie['name'],
@@ -361,7 +269,6 @@ def get_instagram_session():
361
  domain=cookie.get('domain', '.instagram.com')
362
  )
363
 
364
- # Set expiry
365
  IG_SESSION_EXPIRY = datetime.fromisoformat(data['expires'])
366
  global IG_SESSION_USERNAME
367
  IG_SESSION_USERNAME = data.get('username', 'unknown')
@@ -369,34 +276,31 @@ def get_instagram_session():
369
  logger.info(f"Loaded Instagram session for {IG_SESSION_USERNAME}")
370
  return IG_SESSION
371
  else:
372
- logger.warning("Saved session expired, needs new login")
373
  except Exception as e:
374
  logger.error(f"Failed to load cookies: {str(e)}")
375
 
376
- # Return existing session if valid
377
  if IG_SESSION is not None and IG_SESSION_EXPIRY is not None and datetime.now() < IG_SESSION_EXPIRY:
378
  return IG_SESSION
379
 
380
  return None
381
 
382
- def save_instagram_session(session, username, days=7):
383
- """Save Instagram session cookies"""
384
  global IG_SESSION, IG_SESSION_EXPIRY, IG_SESSION_USERNAME
385
 
386
- IG_SESSION = session
387
  IG_SESSION_EXPIRY = datetime.now() + timedelta(days=days)
388
  IG_SESSION_USERNAME = username
389
 
 
 
 
 
 
 
 
390
  try:
391
- cookies = []
392
- for cookie in session.cookies:
393
- cookies.append({
394
- 'name': cookie.name,
395
- 'value': cookie.value,
396
- 'domain': cookie.domain,
397
- 'path': cookie.path
398
- })
399
-
400
  with open(COOKIES_FILE, 'w') as f:
401
  json.dump({
402
  'cookies': cookies,
@@ -411,7 +315,6 @@ def save_instagram_session(session, username, days=7):
411
  return False
412
 
413
  def clear_instagram_session():
414
- """Clear Instagram session"""
415
  global IG_SESSION, IG_SESSION_EXPIRY, IG_SESSION_USERNAME
416
 
417
  IG_SESSION = None
@@ -425,124 +328,46 @@ def clear_instagram_session():
425
  except Exception as e:
426
  logger.error(f"Failed to remove cookies file: {str(e)}")
427
 
428
- def login_to_instagram(username, password):
429
- """Login to Instagram and save session"""
430
- session = requests.Session()
431
-
432
- # Set up headers for web request
433
- session.headers.update({
434
- 'User-Agent': random.choice(USER_AGENTS),
435
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
436
- 'Accept-Language': 'en-US,en;q=0.5',
437
- 'Accept-Encoding': 'gzip, deflate, br',
438
- 'DNT': '1',
439
- 'Connection': 'keep-alive',
440
- 'Upgrade-Insecure-Requests': '1',
441
- 'Sec-Fetch-Dest': 'document',
442
- 'Sec-Fetch-Mode': 'navigate',
443
- 'Sec-Fetch-Site': 'none',
444
- 'Sec-Fetch-User': '?1',
445
- })
446
 
447
- try:
448
- # First get the login page to get CSRF token
449
- response = session.get('https://www.instagram.com/accounts/login/')
450
-
451
- # Wait a bit to look natural
452
- time.sleep(random.uniform(1, 2))
453
-
454
- # Extract CSRF token from response
455
- csrf_token = None
456
- match = re.search(r'"csrf_token":"(.*?)"', response.text)
457
- if match:
458
- csrf_token = match.group(1)
459
-
460
- if not csrf_token:
461
- raise Exception("Could not extract CSRF token")
462
 
463
- # Update headers for login request
464
- session.headers.update({
465
- 'X-CSRFToken': csrf_token,
466
- 'X-Instagram-AJAX': '1',
467
- 'X-Requested-With': 'XMLHttpRequest',
468
- 'Origin': 'https://www.instagram.com',
469
- 'Referer': 'https://www.instagram.com/accounts/login/',
470
- 'Content-Type': 'application/x-www-form-urlencoded'
471
- })
472
-
473
- # Prepare login data
474
- # Convert password to Instagram's browser format
475
- enc_password = f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{password}'
476
-
477
- login_data = {
478
- 'username': username,
479
- 'enc_password': enc_password,
480
- 'queryParams': '{}',
481
- 'optIntoOneTap': 'false'
482
- }
483
-
484
- # Wait a bit more to look natural
485
- time.sleep(random.uniform(2, 3))
486
 
487
- # Make login request
488
- login_response = session.post(
489
- 'https://www.instagram.com/accounts/login/ajax/',
490
- data=login_data
491
- )
492
 
493
- # Check login response
494
- try:
495
- login_json = login_response.json()
496
 
497
- if login_json.get('authenticated') is True:
498
- logger.info(f"Successfully logged in as {username}")
499
-
500
- # Save session
501
- if save_instagram_session(session, username):
502
- return {
503
- 'success': True,
504
- 'message': 'Login successful'
505
- }
506
- else:
507
- return {
508
- 'success': False,
509
- 'message': 'Login successful but failed to save session'
510
- }
511
- else:
512
- if 'message' in login_json:
513
- logger.warning(f"Login failed: {login_json['message']}")
514
- return {
515
- 'success': False,
516
- 'message': f"Login failed: {login_json['message']}"
517
- }
518
- elif 'error_type' in login_json:
519
- logger.warning(f"Login failed: {login_json['error_type']}")
520
- return {
521
- 'success': False,
522
- 'message': f"Login failed: {login_json['error_type']}"
523
- }
524
- else:
525
- logger.warning("Login failed: Unknown reason")
526
- return {
527
- 'success': False,
528
- 'message': 'Login failed for unknown reason'
529
- }
530
- except Exception as e:
531
- logger.error(f"Failed to parse login response: {str(e)}")
532
- return {
533
- 'success': False,
534
- 'message': 'Failed to parse login response'
535
- }
536
-
537
- except Exception as e:
538
- logger.error(f"Login error: {str(e)}")
539
- return {
540
- 'success': False,
541
- 'message': f"Login error: {str(e)}"
542
- }
543
 
544
  def get_instagram_stories(username):
545
- """Get user's stories using the authenticated session"""
546
  session = get_instagram_session()
547
  if not session:
548
  logger.warning("No valid Instagram session")
@@ -554,69 +379,47 @@ def get_instagram_stories(username):
554
 
555
  session.headers.update({
556
  'User-Agent': random.choice(USER_AGENTS),
557
- 'Accept': '*/*',
558
- 'Accept-Language': 'en-US,en;q=0.5',
559
- 'X-IG-App-ID': '936619743392459', # This ID is important
560
- 'X-ASBD-ID': '198387',
561
- 'X-IG-WWW-Claim': '0',
562
- 'Origin': 'https://www.instagram.com',
563
- 'Connection': 'keep-alive',
564
  'Referer': f'https://www.instagram.com/{username}/',
565
- 'Sec-Fetch-Dest': 'empty',
566
- 'Sec-Fetch-Mode': 'cors',
567
- 'Sec-Fetch-Site': 'same-origin',
568
- 'TE': 'trailers',
569
  })
570
 
571
- # Get user profile
572
  profile_response = session.get(profile_url)
573
 
574
  if profile_response.status_code != 200:
575
  logger.warning(f"Failed to get user profile: {profile_response.status_code}")
576
 
577
- if profile_response.status_code == 401:
578
- # Try to extract the message
579
- try:
580
- resp_data = profile_response.json()
581
- if 'message' in resp_data:
582
- logger.warning(f"API error: {resp_data['message']}")
583
- if "wait" in resp_data['message'].lower():
584
- return {"error": "rate_limited", "message": resp_data['message']}
585
- except:
586
- pass
587
-
588
- return {"error": "unauthorized", "message": "Unauthorized access - session may be expired"}
589
 
590
- return {"error": "profile_fetch_failed", "status": profile_response.status_code}
591
 
592
  profile_data = profile_response.json()
593
 
594
  if 'data' not in profile_data or 'user' not in profile_data['data']:
595
- logger.warning("User profile data not found")
596
- return {"error": "profile_not_found", "message": "User profile data not found"}
597
 
598
  user_id = profile_data['data']['user']['id']
599
 
600
- # Small delay
601
  time.sleep(random.uniform(1, 2))
602
 
603
- # Now get the stories
604
  stories_url = f"https://www.instagram.com/api/v1/feed/user/{user_id}/story/"
605
 
606
  stories_response = session.get(stories_url)
607
 
608
  if stories_response.status_code != 200:
609
- logger.warning(f"Failed to get stories: {stories_response.status_code}")
610
- return {"error": "stories_fetch_failed", "status": stories_response.status_code}
611
 
612
  stories_data = stories_response.json()
613
 
614
- # Check if there are stories
615
  if 'reel' not in stories_data or not stories_data['reel'].get('items'):
616
- logger.info(f"No stories found for {username}")
617
- return {"error": "no_stories", "message": "No stories found for this user"}
618
 
619
- # Process stories
620
  stories = []
621
  for item in stories_data['reel']['items']:
622
  story = {
@@ -629,20 +432,18 @@ def get_instagram_stories(username):
629
  if story["type"] == "video" and 'video_versions' in item and item['video_versions']:
630
  story["url"] = item['video_versions'][0]['url']
631
 
632
- # Add view count if available
633
  if 'view_count' in item:
634
  story["views"] = item['view_count']
635
 
636
  elif 'image_versions2' in item and 'candidates' in item['image_versions2'] and item['image_versions2']['candidates']:
637
  story["url"] = item['image_versions2']['candidates'][0]['url']
638
  else:
639
- continue # Skip if no URL
640
 
641
  stories.append(story)
642
 
643
  if not stories:
644
- logger.info(f"No valid stories found for {username}")
645
- return {"error": "no_valid_stories", "message": "No valid stories found for this user"}
646
 
647
  result = {
648
  "data": stories,
@@ -657,79 +458,167 @@ def get_instagram_stories(username):
657
  logger.error(f"Error getting stories: {str(e)}")
658
  return {"error": "exception", "message": str(e)}
659
 
 
660
  @app.get("/", response_class=HTMLResponse)
661
- async def root(request: Request):
662
- """Redirect to status page"""
663
  return RedirectResponse(url="/status")
664
 
665
- @app.get("/login", response_class=HTMLResponse)
666
- async def login_page(request: Request, message: str = None, success: bool = False):
667
- """Login page"""
668
- return templates.TemplateResponse(
669
- "login.html",
670
- {"request": request, "message": message, "success": success}
671
- )
 
672
 
673
- @app.post("/login")
674
- async def login_process(request: Request, username: str = Form(...), password: str = Form(...)):
675
- """Process login form"""
676
- logger.info(f"Login attempt for {username}")
677
 
678
- result = login_to_instagram(username, password)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
- if result['success']:
681
- return RedirectResponse(url="/status", status_code=303)
682
- else:
683
- return templates.TemplateResponse(
684
- "login.html",
685
- {
686
- "request": request, # Now using the actual request object
687
- "message": result['message'],
688
- "success": False
689
- }
690
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
 
692
  @app.get("/logout")
693
  async def logout():
694
- """Logout and clear session"""
695
  clear_instagram_session()
696
  return RedirectResponse(url="/status", status_code=303)
697
 
698
  @app.get("/status", response_class=HTMLResponse)
699
- async def status_page(request: Request):
700
- """Status page"""
701
  session = get_instagram_session()
702
  session_valid = session is not None
703
 
704
- cache_info = {
705
- "count": len(STORY_CACHE),
706
- "items": sum(len(data.get("data", [])) for data in STORY_CACHE.values())
707
- }
708
 
709
- return templates.TemplateResponse(
710
- "status.html",
711
- {
712
- "request": request,
713
- "session_valid": session_valid,
714
- "username": IG_SESSION_USERNAME,
715
- "session_expires": IG_SESSION_EXPIRY.strftime("%Y-%m-%d %H:%M:%S") if IG_SESSION_EXPIRY else None,
716
- "cache_info": cache_info
717
- }
718
- )
 
 
 
 
 
 
 
 
719
 
 
720
  @app.get("/stories/{username}")
721
- async def get_stories(username: str, cached: bool = False, demo: bool = False):
722
- """Get Instagram stories for a user"""
723
  logger.info(f"Request for @{username} stories")
724
 
725
- # Return from cache if requested
726
  if cached:
727
  cached_result = get_cached_stories(username)
728
  if cached_result:
729
  return {**cached_result, "from_cache": True}
730
 
731
  try:
732
- # Check for rate limiting
733
  if should_rate_limit_request(username):
734
  cached_result = get_cached_stories(username)
735
  if cached_result:
@@ -740,38 +629,35 @@ async def get_stories(username: str, cached: bool = False, demo: bool = False):
740
  detail="Rate limit exceeded. Please try again later."
741
  )
742
 
743
- # Get stories
744
  result = get_instagram_stories(username)
745
 
746
  if not result:
747
- # No session or other error
748
  raise HTTPException(
749
  status_code=status.HTTP_401_UNAUTHORIZED,
750
- detail="No valid Instagram session. Please login first."
751
  )
752
 
753
  if "error" in result:
754
- # Handle specific errors
755
- if result["error"] == "rate_limited":
756
  cached_result = get_cached_stories(username)
757
  if cached_result:
758
- return {**cached_result, "from_cache": True, "rate_limited": True}
759
 
760
  raise HTTPException(
761
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
762
- detail=result.get("message", "Rate limit exceeded. Please try again later.")
763
  )
764
 
765
- elif result["error"] == "unauthorized":
766
  raise HTTPException(
767
- status_code=status.HTTP_401_UNAUTHORIZED,
768
- detail=result.get("message", "Unauthorized. Please login again.")
769
  )
770
 
771
- elif result["error"] == "no_stories":
772
  raise HTTPException(
773
  status_code=status.HTTP_404_NOT_FOUND,
774
- detail=result.get("message", "No stories found for this user.")
775
  )
776
 
777
  else:
@@ -780,56 +666,43 @@ async def get_stories(username: str, cached: bool = False, demo: bool = False):
780
  detail=result.get("message", "Failed to get stories")
781
  )
782
 
783
- # Cache the result
784
  save_to_cache(username, result)
785
-
786
  return result
787
 
788
  except HTTPException:
789
- # Re-raise HTTP exceptions
790
  raise
791
  except Exception as e:
792
  logger.error(f"Error: {str(e)}")
793
 
794
- # Check for cached version if there's an error
795
  cached_result = get_cached_stories(username)
796
  if cached_result:
797
  return {**cached_result, "from_cache": True, "error_occurred": True}
798
 
799
  raise HTTPException(
800
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
801
- detail=f"Error getting stories: {str(e)}"
802
  )
803
 
804
  @app.get("/download/{url:path}")
805
  async def download_media(url: str):
806
- """Download and proxy media content"""
807
  logger.info(f"Download request for media")
808
 
809
  try:
810
- # Validate URL
811
  if not url.startswith(("https://", "http://")):
812
  raise HTTPException(
813
  status_code=status.HTTP_400_BAD_REQUEST,
814
  detail="Invalid URL format"
815
  )
816
 
817
- # Configure request
818
  session = get_instagram_session() or requests.Session()
819
  headers = {
820
  "User-Agent": random.choice(USER_AGENTS),
821
- "Accept": "*/*",
822
- "Accept-Language": "en-US,en;q=0.5",
823
- "Accept-Encoding": "gzip, deflate, br",
824
  "Referer": "https://www.instagram.com/",
825
- "Origin": "https://www.instagram.com"
826
  }
827
 
828
- # Get media
829
  response = session.get(url, headers=headers, stream=True, timeout=15)
830
  response.raise_for_status()
831
 
832
- # Determine content type
833
  if "Content-Type" in response.headers:
834
  content_type = response.headers["Content-Type"]
835
  elif url.endswith((".jpg", ".jpeg")):
@@ -841,7 +714,6 @@ async def download_media(url: str):
841
 
842
  logger.info(f"Media downloaded: {content_type}")
843
 
844
- # Return the media
845
  return StreamingResponse(
846
  response.iter_content(chunk_size=8192),
847
  media_type=content_type,
@@ -868,7 +740,6 @@ def startup_event():
868
  cache_data = json.load(f)
869
 
870
  if "data" in cache_data and "expires" in cache_data:
871
- # Convert ISO string to datetime
872
  expires = datetime.fromisoformat(cache_data["expires"])
873
  if expires > datetime.now():
874
  key = filename.replace('.json', '')
 
1
+ from fastapi import FastAPI, HTTPException, status, Request, Form
2
+ from fastapi.responses import StreamingResponse, HTMLResponse, RedirectResponse
 
 
3
  import requests
4
  import os
5
  import time
 
8
  import json
9
  from datetime import datetime, timedelta
10
  import re
 
11
  from typing import Optional
 
 
12
 
13
  # Configure logging
14
  logging.basicConfig(
 
19
 
20
  app = FastAPI(title="Instagram Stories API")
21
 
22
+ # Configuration
23
+ CACHE_DIR = "/tmp/instagram_cache"
24
+ COOKIES_FILE = "/tmp/instagram_cookies.json"
25
+ os.makedirs(CACHE_DIR, exist_ok=True)
 
26
 
27
+ # Instagram session state
28
+ IG_SESSION = None
29
+ IG_SESSION_USERNAME = None
30
+ IG_SESSION_EXPIRY = None
31
+ STORY_CACHE = {}
32
+ CACHE_EXPIRY = {}
33
+ LAST_REQUEST = {}
34
+
35
+ # User agents
36
+ USER_AGENTS = [
37
+ "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",
38
+ "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",
39
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
40
+ ]
41
+
42
+ # Cookie import HTML template
43
+ COOKIE_IMPORT_HTML = """
44
  <!DOCTYPE html>
45
  <html>
46
  <head>
47
+ <title>Import Instagram Session</title>
48
  <style>
49
+ body { font-family: Arial; max-width: 700px; margin: 0 auto; padding: 20px; }
50
+ .container { background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 25px; }
51
+ h1 { color: #262626; text-align: center; }
52
+ .form-group { margin-bottom: 15px; }
53
+ label { display: block; margin-bottom: 10px; }
54
+ textarea { width: 100%; padding: 10px; border: 1px solid #dbdbdb; border-radius: 3px; height: 150px; }
55
+ .btn { background-color: #0095f6; border: none; color: white; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; }
56
+ .btn-block { width: 100%%; }
57
+ .status { margin-top: 20px; padding: 10px; border-radius: 4px; }
58
+ .success { background-color: #e8f5e9; color: #388e3c; }
59
+ .error { background-color: #ffebee; color: #d32f2f; }
60
+ .instructions { background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-bottom: 20px; }
61
+ .code { font-family: monospace; background-color: #f1f1f1; padding: 2px 5px; border-radius: 3px; }
62
+ ol { padding-left: 20px; }
63
+ li { margin-bottom: 10px; }
64
+ .tabs { display: flex; margin-bottom: 20px; }
65
+ .tab { padding: 10px 15px; cursor: pointer; background-color: #f8f9fa; border-radius: 4px 4px 0 0; margin-right: 5px; }
66
+ .tab.active { background-color: #0095f6; color: white; }
67
+ .tab-content { display: none; }
68
+ .tab-content.active { display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  </style>
70
  </head>
71
  <body>
72
  <div class="container">
73
+ <h1>Import Instagram Session</h1>
 
74
 
75
+ <div class="tabs">
76
+ <div class="tab active" onclick="switchTab('tab1', this)">Cookie Format</div>
77
+ <div class="tab" onclick="switchTab('tab2', this)">JSON Format</div>
78
+ </div>
 
79
 
80
+ <div id="tab1" class="tab-content active">
81
+ <div class="instructions">
82
+ <h3>How to get your Instagram cookies:</h3>
83
+ <ol>
84
+ <li>Log in to Instagram in your browser</li>
85
+ <li>Open browser DevTools (F12 or right-click and select "Inspect")</li>
86
+ <li>Go to the "Application" or "Storage" tab</li>
87
+ <li>Under "Cookies", find "instagram.com"</li>
88
+ <li>Find the <span class="code">sessionid</span> cookie and copy its value</li>
89
+ <li>Also copy the <span class="code">ds_user_id</span> and <span class="code">csrftoken</span> cookies if available</li>
90
+ <li>Paste the cookies in the format: <span class="code">name=value; name2=value2;</span></li>
91
+ </ol>
92
  </div>
93
+
94
+ <form method="post" action="/import-cookies">
95
+ <div class="form-group">
96
+ <label for="cookies">Paste your Instagram cookies here:</label>
97
+ <textarea id="cookies" name="cookies" placeholder="sessionid=YOUR_SESSION_ID; ds_user_id=YOUR_USER_ID; csrftoken=YOUR_CSRF_TOKEN;" required></textarea>
98
+ </div>
99
+ <div class="form-group">
100
+ <label for="username">Your Instagram username:</label>
101
+ <input type="text" id="username" name="username" style="width: 100%; padding: 10px; border: 1px solid #dbdbdb; border-radius: 3px;" required>
102
+ </div>
103
+ <button type="submit" class="btn btn-block">Import Session</button>
104
+ </form>
105
+ </div>
106
+
107
+ <div id="tab2" class="tab-content">
108
+ <div class="instructions">
109
+ <h3>How to get your cookies as JSON:</h3>
110
+ <ol>
111
+ <li>Install a browser extension like "EditThisCookie" or "Cookie-Editor"</li>
112
+ <li>Log in to Instagram in your browser</li>
113
+ <li>Click on the extension icon</li>
114
+ <li>Choose "Export" or similar option to copy all cookies</li>
115
+ <li>Paste the JSON data below</li>
116
+ </ol>
117
  </div>
118
+
119
+ <form method="post" action="/import-cookies-json">
120
+ <div class="form-group">
121
+ <label for="cookies_json">Paste your Instagram cookies JSON here:</label>
122
+ <textarea id="cookies_json" name="cookies_json" placeholder='[{"name":"sessionid","value":"YOUR_SESSION_ID","domain":".instagram.com"}]' required></textarea>
123
+ </div>
124
+ <div class="form-group">
125
+ <label for="username_json">Your Instagram username:</label>
126
+ <input type="text" id="username_json" name="username" style="width: 100%; padding: 10px; border: 1px solid #dbdbdb; border-radius: 3px;" required>
127
+ </div>
128
+ <button type="submit" class="btn btn-block">Import Session</button>
129
+ </form>
130
+ </div>
131
+
132
+ %s
133
  </div>
134
+
135
+ <script>
136
+ function switchTab(tabId, tabElement) {
137
+ // Hide all tab contents
138
+ document.querySelectorAll('.tab-content').forEach(content => {
139
+ content.classList.remove('active');
140
+ });
141
+
142
+ // Deactivate all tabs
143
+ document.querySelectorAll('.tab').forEach(tab => {
144
+ tab.classList.remove('active');
145
+ });
146
+
147
+ // Activate clicked tab and its content
148
+ document.getElementById(tabId).classList.add('active');
149
+ tabElement.classList.add('active');
150
+ }
151
+ </script>
152
  </body>
153
  </html>
154
  """
155
 
156
+ # Status page HTML
157
+ STATUS_HTML = """
158
  <!DOCTYPE html>
159
  <html>
160
  <head>
161
+ <title>API Status</title>
162
  <style>
163
+ body { font-family: Arial; max-width: 600px; margin: 0 auto; padding: 20px; }
164
+ .container { background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 25px; }
165
+ h1 { color: #262626; text-align: center; }
166
+ .status { display: inline-block; padding: 5px 10px; border-radius: 4px; margin-left: 10px; font-weight: bold; }
167
+ .connected { background-color: #e8f5e9; color: #388e3c; }
168
+ .disconnected { background-color: #ffebee; color: #d32f2f; }
169
+ .info { background-color: #f5f5f5; border-radius: 4px; padding: 15px; margin: 15px 0; }
170
+ .btn { background-color: #0095f6; border: none; color: white; padding: 10px 15px; border-radius: 4px; text-decoration: none; display: inline-block; margin-right: 10px; }
171
+ .btn-danger { background-color: #f44336; }
172
+ table { width: 100%%; border-collapse: collapse; margin-top: 15px; }
173
+ th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </style>
175
  </head>
176
  <body>
 
178
  <h1>Instagram API Status</h1>
179
 
180
  <h2>Connection Status
181
+ %s
 
 
 
 
182
  </h2>
183
 
184
+ <div class="info">
185
+ %s
 
 
 
 
 
 
186
  </div>
187
 
188
+ <a href="/import" class="btn">%s</a>
189
+ %s
 
 
190
 
191
  <h2>API Endpoints</h2>
192
  <table>
193
+ <tr><th>Endpoint</th><th>Description</th></tr>
194
+ <tr><td><code>/stories/{username}</code></td><td>Get stories for a user</td></tr>
195
+ <tr><td><code>/download/{url}</code></td><td>Download media from a URL</td></tr>
196
+ <tr><td><code>/status</code></td><td>View API status</td></tr>
 
 
 
 
 
 
 
 
 
 
 
 
197
  </table>
198
 
199
+ <h2>Cache Info</h2>
200
+ <div class="info">
201
+ <p>Cached users: %d</p>
202
+ <p>Total stories cached: %d</p>
 
203
  </div>
 
204
  </div>
205
  </body>
206
  </html>
207
  """
208
 
209
+ # Functions for cache and Instagram session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  def get_cache_key(username):
 
211
  return username.lower()
212
 
213
  def get_cached_stories(username):
 
214
  key = get_cache_key(username)
215
  if key in STORY_CACHE and key in CACHE_EXPIRY:
216
  if datetime.now() < CACHE_EXPIRY[key]:
217
  logger.info(f"Cache hit for {username}")
218
  return STORY_CACHE[key]
219
  else:
 
220
  STORY_CACHE.pop(key, None)
221
  CACHE_EXPIRY.pop(key, None)
222
  return None
223
 
224
  def save_to_cache(username, data, minutes=30):
 
225
  key = get_cache_key(username)
226
  STORY_CACHE[key] = data
227
  CACHE_EXPIRY[key] = datetime.now() + timedelta(minutes=minutes)
228
 
 
229
  try:
230
  cache_file = os.path.join(CACHE_DIR, f"{key}.json")
231
  with open(cache_file, 'w') as f:
 
238
  logger.warning(f"Failed to save cache: {str(e)}")
239
 
240
  def should_rate_limit_request(username):
 
241
  key = username.lower()
242
  now = datetime.now()
243
 
244
  if key in LAST_REQUEST:
245
  seconds_since = (now - LAST_REQUEST[key]).total_seconds()
246
+ if seconds_since < 60:
247
  logger.warning(f"Rate limiting {username}: {seconds_since:.1f}s since last request")
248
  return True
249
 
 
251
  return False
252
 
253
  def get_instagram_session():
 
254
  global IG_SESSION, IG_SESSION_EXPIRY
255
 
 
256
  if IG_SESSION is None or IG_SESSION_EXPIRY is None or datetime.now() > IG_SESSION_EXPIRY:
 
257
  if os.path.exists(COOKIES_FILE):
258
  try:
259
  with open(COOKIES_FILE, 'r') as f:
 
262
  if 'expires' in data and datetime.fromisoformat(data['expires']) > datetime.now():
263
  IG_SESSION = requests.Session()
264
 
 
265
  for cookie in data['cookies']:
266
  IG_SESSION.cookies.set(
267
  cookie['name'],
 
269
  domain=cookie.get('domain', '.instagram.com')
270
  )
271
 
 
272
  IG_SESSION_EXPIRY = datetime.fromisoformat(data['expires'])
273
  global IG_SESSION_USERNAME
274
  IG_SESSION_USERNAME = data.get('username', 'unknown')
 
276
  logger.info(f"Loaded Instagram session for {IG_SESSION_USERNAME}")
277
  return IG_SESSION
278
  else:
279
+ logger.warning("Saved session expired, needs new import")
280
  except Exception as e:
281
  logger.error(f"Failed to load cookies: {str(e)}")
282
 
 
283
  if IG_SESSION is not None and IG_SESSION_EXPIRY is not None and datetime.now() < IG_SESSION_EXPIRY:
284
  return IG_SESSION
285
 
286
  return None
287
 
288
+ def save_instagram_session(cookies, username, days=30):
289
+ """Save imported cookie data"""
290
  global IG_SESSION, IG_SESSION_EXPIRY, IG_SESSION_USERNAME
291
 
292
+ IG_SESSION = requests.Session()
293
  IG_SESSION_EXPIRY = datetime.now() + timedelta(days=days)
294
  IG_SESSION_USERNAME = username
295
 
296
+ for cookie in cookies:
297
+ IG_SESSION.cookies.set(
298
+ cookie['name'],
299
+ cookie['value'],
300
+ domain=cookie.get('domain', '.instagram.com')
301
+ )
302
+
303
  try:
 
 
 
 
 
 
 
 
 
304
  with open(COOKIES_FILE, 'w') as f:
305
  json.dump({
306
  'cookies': cookies,
 
315
  return False
316
 
317
  def clear_instagram_session():
 
318
  global IG_SESSION, IG_SESSION_EXPIRY, IG_SESSION_USERNAME
319
 
320
  IG_SESSION = None
 
328
  except Exception as e:
329
  logger.error(f"Failed to remove cookies file: {str(e)}")
330
 
331
+ def parse_cookie_string(cookie_str):
332
+ """Parse cookie string into cookie objects"""
333
+ cookies = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
+ for cookie_part in cookie_str.split(';'):
336
+ cookie_part = cookie_part.strip()
337
+ if not cookie_part:
338
+ continue
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ if '=' in cookie_part:
341
+ name, value = cookie_part.split('=', 1)
342
+ cookies.append({
343
+ 'name': name.strip(),
344
+ 'value': value.strip(),
345
+ 'domain': '.instagram.com'
346
+ })
347
+
348
+ return cookies
349
+
350
+ def validate_imported_session(session):
351
+ """Check if the imported session seems valid"""
352
+ # Check for essential cookies
353
+ has_sessionid = any(c['name'] == 'sessionid' for c in session.cookies._cookies.get('.instagram.com', {}).get('/', {}).values())
354
+
355
+ if not has_sessionid:
356
+ return False
 
 
 
 
 
 
357
 
358
+ # Try to make a basic request
359
+ try:
360
+ response = session.get('https://www.instagram.com/accounts/edit/', allow_redirects=False)
 
 
361
 
362
+ # If we get redirected to login page, session is invalid
363
+ if response.status_code == 302 and 'login' in response.headers.get('Location', ''):
364
+ return False
365
 
366
+ return response.status_code == 200
367
+ except:
368
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
  def get_instagram_stories(username):
 
371
  session = get_instagram_session()
372
  if not session:
373
  logger.warning("No valid Instagram session")
 
379
 
380
  session.headers.update({
381
  'User-Agent': random.choice(USER_AGENTS),
382
+ 'X-IG-App-ID': '936619743392459',
383
+ 'X-IG-WWW-Claim': 'hmac.AR1k18eNmvbsN7COEa9jpSQ9-NKGlK_TVqhgqxaGgxdCsIfR',
 
 
 
 
 
384
  'Referer': f'https://www.instagram.com/{username}/',
 
 
 
 
385
  })
386
 
 
387
  profile_response = session.get(profile_url)
388
 
389
  if profile_response.status_code != 200:
390
  logger.warning(f"Failed to get user profile: {profile_response.status_code}")
391
 
392
+ try:
393
+ resp_data = profile_response.json()
394
+ if 'message' in resp_data:
395
+ return {"error": "api_error", "message": resp_data['message']}
396
+ except:
397
+ pass
 
 
 
 
 
 
398
 
399
+ return {"error": "profile_error", "status": profile_response.status_code}
400
 
401
  profile_data = profile_response.json()
402
 
403
  if 'data' not in profile_data or 'user' not in profile_data['data']:
404
+ return {"error": "not_found", "message": "User profile not found"}
 
405
 
406
  user_id = profile_data['data']['user']['id']
407
 
 
408
  time.sleep(random.uniform(1, 2))
409
 
410
+ # Get the stories
411
  stories_url = f"https://www.instagram.com/api/v1/feed/user/{user_id}/story/"
412
 
413
  stories_response = session.get(stories_url)
414
 
415
  if stories_response.status_code != 200:
416
+ return {"error": "stories_error", "status": stories_response.status_code}
 
417
 
418
  stories_data = stories_response.json()
419
 
 
420
  if 'reel' not in stories_data or not stories_data['reel'].get('items'):
421
+ return {"error": "no_stories", "message": "No stories found"}
 
422
 
 
423
  stories = []
424
  for item in stories_data['reel']['items']:
425
  story = {
 
432
  if story["type"] == "video" and 'video_versions' in item and item['video_versions']:
433
  story["url"] = item['video_versions'][0]['url']
434
 
 
435
  if 'view_count' in item:
436
  story["views"] = item['view_count']
437
 
438
  elif 'image_versions2' in item and 'candidates' in item['image_versions2'] and item['image_versions2']['candidates']:
439
  story["url"] = item['image_versions2']['candidates'][0]['url']
440
  else:
441
+ continue
442
 
443
  stories.append(story)
444
 
445
  if not stories:
446
+ return {"error": "no_valid_stories", "message": "No valid stories found"}
 
447
 
448
  result = {
449
  "data": stories,
 
458
  logger.error(f"Error getting stories: {str(e)}")
459
  return {"error": "exception", "message": str(e)}
460
 
461
+ # Web routes
462
  @app.get("/", response_class=HTMLResponse)
463
+ async def root():
 
464
  return RedirectResponse(url="/status")
465
 
466
+ @app.get("/import", response_class=HTMLResponse)
467
+ async def import_page(message: str = None, success: bool = False):
468
+ status_html = ""
469
+ if message:
470
+ status_class = "success" if success else "error"
471
+ status_html = f'<div class="status {status_class}">{message}</div>'
472
+
473
+ return HTMLResponse(COOKIE_IMPORT_HTML % status_html)
474
 
475
+ @app.post("/import-cookies")
476
+ async def import_cookies(cookies: str = Form(...), username: str = Form(...)):
477
+ logger.info(f"Importing cookies for {username}")
 
478
 
479
+ try:
480
+ cookie_objects = parse_cookie_string(cookies)
481
+
482
+ if not cookie_objects:
483
+ logger.warning("No valid cookies found in input")
484
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">No valid cookies found in input</div>')
485
+
486
+ # Check for essential cookies
487
+ has_sessionid = any(c['name'] == 'sessionid' for c in cookie_objects)
488
+
489
+ if not has_sessionid:
490
+ logger.warning("Missing essential sessionid cookie")
491
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Missing essential sessionid cookie</div>')
492
+
493
+ # Save session
494
+ if save_instagram_session(cookie_objects, username):
495
+ # Verify session
496
+ session = get_instagram_session()
497
+ if session:
498
+ # Try to make a basic request to test session
499
+ try:
500
+ response = session.get('https://www.instagram.com/accounts/edit/', allow_redirects=False)
501
+
502
+ if response.status_code == 302 and 'login' in response.headers.get('Location', ''):
503
+ logger.warning("Session seems invalid - redirected to login")
504
+ clear_instagram_session()
505
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Session validation failed - redirected to login</div>')
506
+
507
+ logger.info("Session validated successfully")
508
+ return RedirectResponse(url="/status", status_code=303)
509
+ except Exception as e:
510
+ logger.error(f"Session validation error: {str(e)}")
511
+ return HTMLResponse(COOKIE_IMPORT_HTML % f'<div class="status error">Session validation error: {str(e)}</div>')
512
+ else:
513
+ logger.error("Failed to get session after saving")
514
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Failed to create session</div>')
515
+ else:
516
+ logger.error("Failed to save session")
517
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Failed to save session</div>')
518
 
519
+ except Exception as e:
520
+ logger.error(f"Import error: {str(e)}")
521
+ return HTMLResponse(COOKIE_IMPORT_HTML % f'<div class="status error">Import error: {str(e)}</div>')
522
+
523
+ @app.post("/import-cookies-json")
524
+ async def import_cookies_json(cookies_json: str = Form(...), username: str = Form(...)):
525
+ logger.info(f"Importing JSON cookies for {username}")
526
+
527
+ try:
528
+ cookie_objects = json.loads(cookies_json)
529
+
530
+ if not isinstance(cookie_objects, list):
531
+ logger.warning("Invalid JSON format - expected array")
532
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Invalid JSON format - expected array of cookie objects</div>')
533
+
534
+ # Check for essential cookies
535
+ has_sessionid = any(c.get('name') == 'sessionid' for c in cookie_objects)
536
+
537
+ if not has_sessionid:
538
+ logger.warning("Missing essential sessionid cookie")
539
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Missing essential sessionid cookie</div>')
540
+
541
+ # Normalize cookie objects
542
+ for cookie in cookie_objects:
543
+ if 'domain' not in cookie:
544
+ cookie['domain'] = '.instagram.com'
545
+
546
+ # Save session
547
+ if save_instagram_session(cookie_objects, username):
548
+ # Verify session
549
+ session = get_instagram_session()
550
+ if session:
551
+ # Try to make a basic request to test session
552
+ try:
553
+ response = session.get('https://www.instagram.com/accounts/edit/', allow_redirects=False)
554
+
555
+ if response.status_code == 302 and 'login' in response.headers.get('Location', ''):
556
+ logger.warning("Session seems invalid - redirected to login")
557
+ clear_instagram_session()
558
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Session validation failed - redirected to login</div>')
559
+
560
+ logger.info("Session validated successfully")
561
+ return RedirectResponse(url="/status", status_code=303)
562
+ except Exception as e:
563
+ logger.error(f"Session validation error: {str(e)}")
564
+ return HTMLResponse(COOKIE_IMPORT_HTML % f'<div class="status error">Session validation error: {str(e)}</div>')
565
+ else:
566
+ logger.error("Failed to get session after saving")
567
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Failed to create session</div>')
568
+ else:
569
+ logger.error("Failed to save session")
570
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Failed to save session</div>')
571
+
572
+ except json.JSONDecodeError:
573
+ logger.error("Invalid JSON format")
574
+ return HTMLResponse(COOKIE_IMPORT_HTML % '<div class="status error">Invalid JSON format</div>')
575
+ except Exception as e:
576
+ logger.error(f"Import error: {str(e)}")
577
+ return HTMLResponse(COOKIE_IMPORT_HTML % f'<div class="status error">Import error: {str(e)}</div>')
578
 
579
  @app.get("/logout")
580
  async def logout():
 
581
  clear_instagram_session()
582
  return RedirectResponse(url="/status", status_code=303)
583
 
584
  @app.get("/status", response_class=HTMLResponse)
585
+ async def status_page():
 
586
  session = get_instagram_session()
587
  session_valid = session is not None
588
 
589
+ # Status badge
590
+ status_badge = '<span class="status connected">Connected</span>' if session_valid else '<span class="status disconnected">Disconnected</span>'
 
 
591
 
592
+ # Status info
593
+ if session_valid:
594
+ status_info = f"<p>✅ You are currently logged in as <strong>{IG_SESSION_USERNAME}</strong></p>"
595
+ status_info += f"<p>Session will expire: {IG_SESSION_EXPIRY.strftime('%Y-%m-%d %H:%M:%S')}</p>"
596
+ else:
597
+ status_info = "<p>❌ No active Instagram session</p><p>Please import your cookies to use the API</p>"
598
+
599
+ # Button text
600
+ import_text = "Re-Import Cookies" if session_valid else "Import Cookies"
601
+
602
+ # Logout button
603
+ logout_button = '<a href="/logout" class="btn btn-danger">Logout</a>' if session_valid else ''
604
+
605
+ # Cache info
606
+ cache_count = len(STORY_CACHE)
607
+ story_count = sum(len(data.get("data", [])) for data in STORY_CACHE.values())
608
+
609
+ return HTMLResponse(STATUS_HTML % (status_badge, status_info, import_text, logout_button, cache_count, story_count))
610
 
611
+ # API routes
612
  @app.get("/stories/{username}")
613
+ async def get_stories(username: str, cached: bool = False):
 
614
  logger.info(f"Request for @{username} stories")
615
 
 
616
  if cached:
617
  cached_result = get_cached_stories(username)
618
  if cached_result:
619
  return {**cached_result, "from_cache": True}
620
 
621
  try:
 
622
  if should_rate_limit_request(username):
623
  cached_result = get_cached_stories(username)
624
  if cached_result:
 
629
  detail="Rate limit exceeded. Please try again later."
630
  )
631
 
 
632
  result = get_instagram_stories(username)
633
 
634
  if not result:
 
635
  raise HTTPException(
636
  status_code=status.HTTP_401_UNAUTHORIZED,
637
+ detail="No valid Instagram session. Please import your cookies first."
638
  )
639
 
640
  if "error" in result:
641
+ if result["error"] == "api_error":
 
642
  cached_result = get_cached_stories(username)
643
  if cached_result:
644
+ return {**cached_result, "from_cache": True, "error_occurred": True}
645
 
646
  raise HTTPException(
647
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
648
+ detail=result.get("message", "Instagram API error")
649
  )
650
 
651
+ elif result["error"] in ["profile_error", "not_found"]:
652
  raise HTTPException(
653
+ status_code=status.HTTP_404_NOT_FOUND,
654
+ detail="Profile not found"
655
  )
656
 
657
+ elif result["error"] in ["no_stories", "no_valid_stories"]:
658
  raise HTTPException(
659
  status_code=status.HTTP_404_NOT_FOUND,
660
+ detail="No stories found for this user"
661
  )
662
 
663
  else:
 
666
  detail=result.get("message", "Failed to get stories")
667
  )
668
 
 
669
  save_to_cache(username, result)
 
670
  return result
671
 
672
  except HTTPException:
 
673
  raise
674
  except Exception as e:
675
  logger.error(f"Error: {str(e)}")
676
 
 
677
  cached_result = get_cached_stories(username)
678
  if cached_result:
679
  return {**cached_result, "from_cache": True, "error_occurred": True}
680
 
681
  raise HTTPException(
682
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
683
+ detail=str(e)
684
  )
685
 
686
  @app.get("/download/{url:path}")
687
  async def download_media(url: str):
 
688
  logger.info(f"Download request for media")
689
 
690
  try:
 
691
  if not url.startswith(("https://", "http://")):
692
  raise HTTPException(
693
  status_code=status.HTTP_400_BAD_REQUEST,
694
  detail="Invalid URL format"
695
  )
696
 
 
697
  session = get_instagram_session() or requests.Session()
698
  headers = {
699
  "User-Agent": random.choice(USER_AGENTS),
 
 
 
700
  "Referer": "https://www.instagram.com/",
 
701
  }
702
 
 
703
  response = session.get(url, headers=headers, stream=True, timeout=15)
704
  response.raise_for_status()
705
 
 
706
  if "Content-Type" in response.headers:
707
  content_type = response.headers["Content-Type"]
708
  elif url.endswith((".jpg", ".jpeg")):
 
714
 
715
  logger.info(f"Media downloaded: {content_type}")
716
 
 
717
  return StreamingResponse(
718
  response.iter_content(chunk_size=8192),
719
  media_type=content_type,
 
740
  cache_data = json.load(f)
741
 
742
  if "data" in cache_data and "expires" in cache_data:
 
743
  expires = datetime.fromisoformat(cache_data["expires"])
744
  if expires > datetime.now():
745
  key = filename.replace('.json', '')