Chrunos commited on
Commit
3e4af20
·
verified ·
1 Parent(s): cce0826

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -103
app.py CHANGED
@@ -5,6 +5,7 @@ import requests
5
  import os
6
  import time
7
  import logging
 
8
  from functools import wraps
9
  from typing import Optional
10
 
@@ -21,81 +22,105 @@ app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
21
  INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
22
  INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
23
 
24
- # Session file path
25
- SESSION_FILE = f"/tmp/session-{INSTAGRAM_USERNAME}" if INSTAGRAM_USERNAME else None
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def get_instaloader() -> instaloader.Instaloader:
28
- """Create and configure Instaloader instance with session management"""
29
  L = instaloader.Instaloader(
30
  sleep=True,
31
  request_timeout=300,
32
- max_connection_attempts=3,
33
- user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 
34
  )
35
 
36
  if not INSTAGRAM_USERNAME or not INSTAGRAM_PASSWORD:
37
- logger.error("Instagram credentials not configured in environment variables")
38
  raise HTTPException(
39
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
40
  detail="Server configuration error"
41
  )
42
 
43
  try:
 
44
  if SESSION_FILE and os.path.exists(SESSION_FILE):
45
- logger.info(f"Loading session from file: {SESSION_FILE}")
46
- L.load_session_from_file(INSTAGRAM_USERNAME, SESSION_FILE)
47
- else:
48
- logger.info("No session file found, performing fresh login")
49
- L.login(INSTAGRAM_USERNAME, INSTAGRAM_PASSWORD)
50
- if SESSION_FILE:
51
- L.save_session_to_file(SESSION_FILE)
52
- logger.info(f"Saved new session to: {SESSION_FILE}")
53
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  except instaloader.exceptions.BadCredentialsException as e:
55
- logger.error("Invalid Instagram credentials: %s", str(e))
56
  raise HTTPException(
57
  status_code=status.HTTP_401_UNAUTHORIZED,
58
  detail="Invalid Instagram credentials"
59
  )
60
- except instaloader.exceptions.ConnectionException as e:
61
- logger.error("Instagram connection failed: %s", str(e))
62
  raise HTTPException(
63
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
64
- detail="Instagram service unavailable"
65
  )
66
  except Exception as e:
67
- logger.error("Unexpected error during session creation: %s", str(e))
68
  raise HTTPException(
69
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
70
- detail="Internal server error"
71
  )
72
 
73
- # Configure rate limiting
74
- L.delay_seconds = 10 # Increased delay between requests
75
- return L
76
-
77
  def handle_instagram_errors(func):
78
  @wraps(func)
79
  async def wrapper(*args, **kwargs):
80
  try:
81
  return await func(*args, **kwargs)
82
  except instaloader.exceptions.QueryReturnedBadRequestException as e:
83
- logger.error("Instagram API error (400): %s", str(e))
84
  raise HTTPException(
85
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
86
- detail="Too many requests to Instagram - try again later"
87
  )
88
  except instaloader.exceptions.ConnectionException as e:
89
  logger.error("Connection error: %s", str(e))
90
  raise HTTPException(
91
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
92
- detail="Instagram connection failed"
93
- )
94
- except instaloader.exceptions.ProfileNotExistsException as e:
95
- logger.warning("Profile not found: %s", str(e))
96
- raise HTTPException(
97
- status_code=status.HTTP_404_NOT_FOUND,
98
- detail="Profile not found"
99
  )
100
  except Exception as e:
101
  logger.error("Unexpected error: %s", str(e))
@@ -108,25 +133,46 @@ def handle_instagram_errors(func):
108
  @app.get("/stories/{username}")
109
  @handle_instagram_errors
110
  async def get_stories(username: str):
111
- """Retrieve Instagram stories with enhanced logging"""
112
- logger.info(f"Received request for stories from @{username}")
113
 
114
  try:
115
  L = get_instaloader()
116
- logger.debug("Instaloader instance created")
117
 
118
- logger.info(f"Attempting to fetch profile for @{username}")
119
- profile = instaloader.Profile.from_username(L.context, username)
120
- logger.info(f"Successfully retrieved profile: {profile.userid}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
 
122
  if not profile.has_public_story and not profile.is_followed_by_viewer:
123
- logger.warning(f"No accessible stories for @{username}")
124
  raise HTTPException(
125
  status.HTTP_403_FORBIDDEN,
126
  "No accessible stories for this profile"
127
  )
128
 
129
- logger.info(f"Fetching stories for user ID: {profile.userid}")
 
 
 
130
  stories = []
131
  try:
132
  for story in L.get_stories(userids=[profile.userid]):
@@ -138,77 +184,28 @@ async def get_stories(username: str):
138
  "timestamp": item.date_utc.isoformat(),
139
  "views": item.view_count if item.is_video else None,
140
  })
141
- logger.debug(f"Found story item: {item.mediaid}")
142
- except instaloader.exceptions.QueryReturnedNotFoundException as e:
143
- logger.error(f"Story query failed: {str(e)}")
144
  raise HTTPException(
145
  status.HTTP_404_NOT_FOUND,
146
- "Stories not found or expired"
147
  )
148
 
149
  if not stories:
150
- logger.info(f"No active stories found for @{username}")
151
  raise HTTPException(
152
  status.HTTP_404_NOT_FOUND,
153
  "No active stories available"
154
  )
155
 
156
- logger.info(f"Returning {len(stories)} stories for @{username}")
 
 
 
157
  return {"data": stories}
158
 
159
  except Exception as e:
160
- logger.error(f"Critical error in get_stories: {str(e)}")
161
- raise
162
-
163
- @app.get("/download/{media_id}")
164
- @handle_instagram_errors
165
- async def download_story(media_id: str, username: str):
166
- """Download story media with retry logic"""
167
- logger.info(f"Download request for media {media_id} from @{username}")
168
-
169
- L = get_instaloader()
170
- try:
171
- profile = instaloader.Profile.from_username(L.context, username)
172
- except Exception as e:
173
- logger.error(f"Profile fetch failed for @{username}: {str(e)}")
174
  raise
175
 
176
- story_item = None
177
- for attempt in range(3):
178
- try:
179
- logger.info(f"Searching for media {media_id} (attempt {attempt+1}/3)")
180
- story_item = next(
181
- (item for story in L.get_stories(userids=[profile.userid])
182
- for item in story.get_items()
183
- if str(item.mediaid) == media_id
184
- ), None)
185
- if story_item:
186
- break
187
- except instaloader.exceptions.QueryReturnedBadRequestException:
188
- wait_time = (2 ** attempt) * 10 # Exponential backoff
189
- logger.warning(f"Rate limited, waiting {wait_time}s before retry")
190
- time.sleep(wait_time)
191
-
192
- if not story_item:
193
- logger.error(f"Media {media_id} not found for @{username}")
194
- raise HTTPException(status.HTTP_404_NOT_FOUND, "Story not found")
195
-
196
- try:
197
- logger.info(f"Downloading media from {story_item.url}")
198
- response = L.context.get(story_item.url, stream=True)
199
- response.raise_for_status()
200
- except requests.exceptions.RequestException as e:
201
- logger.error(f"Media download failed: {str(e)}")
202
- raise HTTPException(
203
- status.HTTP_500_INTERNAL_SERVER_ERROR,
204
- "Failed to fetch media content"
205
- )
206
-
207
- return StreamingResponse(
208
- response.iter_content(chunk_size=8192),
209
- media_type="video/mp4" if story_item.is_video else "image/jpeg",
210
- headers={
211
- "Content-Disposition": f"attachment; filename={username}_{media_id}"
212
- f".{'mp4' if story_item.is_video else 'jpg'}"
213
- }
214
- )
 
5
  import os
6
  import time
7
  import logging
8
+ import random
9
  from functools import wraps
10
  from typing import Optional
11
 
 
22
  INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
23
  INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
24
 
25
+ # Session configuration
26
+ SESSION_DIR = "/tmp/instagram_sessions"
27
+ SESSION_FILE = os.path.join(SESSION_DIR, f"session-{INSTAGRAM_USERNAME}") if INSTAGRAM_USERNAME else None
28
+
29
+ # Create session directory if not exists
30
+ os.makedirs(SESSION_DIR, exist_ok=True)
31
+
32
+ # Realistic browser user agents
33
+ USER_AGENTS = [
34
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
35
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15",
36
+ "Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
37
+ ]
38
 
39
  def get_instaloader() -> instaloader.Instaloader:
40
+ """Create and configure Instaloader instance with advanced session handling"""
41
  L = instaloader.Instaloader(
42
  sleep=True,
43
  request_timeout=300,
44
+ max_connection_attempts=2,
45
+ user_agent=random.choice(USER_AGENTS),
46
+ sleep_seconds=15 # Increased initial delay
47
  )
48
 
49
  if not INSTAGRAM_USERNAME or not INSTAGRAM_PASSWORD:
50
+ logger.error("Instagram credentials not configured")
51
  raise HTTPException(
52
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
53
  detail="Server configuration error"
54
  )
55
 
56
  try:
57
+ # Attempt to load existing session with retry logic
58
  if SESSION_FILE and os.path.exists(SESSION_FILE):
59
+ logger.info(f"Attempting to load session from {SESSION_FILE}")
60
+ try:
61
+ L.load_session_from_file(INSTAGRAM_USERNAME, SESSION_FILE)
62
+ logger.info("Session loaded successfully")
63
+ # Verify session is still valid
64
+ test_profile = instaloader.Profile.from_username(L.context, INSTAGRAM_USERNAME)
65
+ if not test_profile.is_verified:
66
+ raise Exception("Session validation failed")
67
+ return L
68
+ except Exception as e:
69
+ logger.warning(f"Session load failed: {str(e)}, performing fresh login")
70
+
71
+ # Fresh login with additional headers
72
+ logger.info("Starting fresh login process")
73
+ L.login(INSTAGRAM_USERNAME, INSTAGRAM_PASSWORD)
74
+
75
+ # Add additional headers to mimic browser behavior
76
+ L.context._session.headers.update({
77
+ 'Accept-Language': 'en-US,en;q=0.9',
78
+ 'Accept-Encoding': 'gzip, deflate, br',
79
+ 'Referer': 'https://www.instagram.com/',
80
+ 'DNT': '1',
81
+ })
82
+
83
+ if SESSION_FILE:
84
+ L.save_session_to_file(SESSION_FILE)
85
+ logger.info(f"Saved new session to {SESSION_FILE}")
86
+
87
+ return L
88
+
89
  except instaloader.exceptions.BadCredentialsException as e:
90
+ logger.error("Invalid credentials: %s", str(e))
91
  raise HTTPException(
92
  status_code=status.HTTP_401_UNAUTHORIZED,
93
  detail="Invalid Instagram credentials"
94
  )
95
+ except instaloader.exceptions.TwoFactorAuthRequiredException as e:
96
+ logger.error("2FA required: %s", str(e))
97
  raise HTTPException(
98
+ status_code=status.HTTP_401_UNAUTHORIZED,
99
+ detail="Two-factor authentication required"
100
  )
101
  except Exception as e:
102
+ logger.error("Login failed: %s", str(e))
103
  raise HTTPException(
104
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
105
+ detail="Instagram login service unavailable"
106
  )
107
 
 
 
 
 
108
  def handle_instagram_errors(func):
109
  @wraps(func)
110
  async def wrapper(*args, **kwargs):
111
  try:
112
  return await func(*args, **kwargs)
113
  except instaloader.exceptions.QueryReturnedBadRequestException as e:
114
+ logger.error("API error 400: %s", str(e))
115
  raise HTTPException(
116
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
117
+ detail="Instagram rate limit exceeded"
118
  )
119
  except instaloader.exceptions.ConnectionException as e:
120
  logger.error("Connection error: %s", str(e))
121
  raise HTTPException(
122
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
123
+ detail="Instagram service unavailable"
 
 
 
 
 
 
124
  )
125
  except Exception as e:
126
  logger.error("Unexpected error: %s", str(e))
 
133
  @app.get("/stories/{username}")
134
  @handle_instagram_errors
135
  async def get_stories(username: str):
136
+ """Retrieve stories with enhanced anti-detection measures"""
137
+ logger.info(f"Processing request for @{username}")
138
 
139
  try:
140
  L = get_instaloader()
141
+ logger.info("Instaloader instance configured")
142
 
143
+ # Randomized delay before profile request
144
+ delay = random.uniform(2.5, 5.5)
145
+ logger.debug(f"Applying initial delay of {delay:.2f}s")
146
+ time.sleep(delay)
147
+
148
+ # Profile lookup with retry
149
+ for attempt in range(3):
150
+ try:
151
+ logger.info(f"Fetching profile (attempt {attempt+1}/3)")
152
+ profile = instaloader.Profile.from_username(L.context, username)
153
+ break
154
+ except instaloader.exceptions.QueryReturnedBadRequestException:
155
+ wait_time = (attempt + 1) * 10
156
+ logger.warning(f"Rate limited, waiting {wait_time}s")
157
+ time.sleep(wait_time)
158
+ else:
159
+ raise HTTPException(
160
+ status.HTTP_429_TOO_MANY_REQUESTS,
161
+ "Too many attempts to access Instagram"
162
+ )
163
 
164
+ logger.info(f"Access check for @{username}")
165
  if not profile.has_public_story and not profile.is_followed_by_viewer:
166
+ logger.warning("Private profile access denied")
167
  raise HTTPException(
168
  status.HTTP_403_FORBIDDEN,
169
  "No accessible stories for this profile"
170
  )
171
 
172
+ # Additional delay before story fetch
173
+ time.sleep(random.uniform(1.5, 3.5))
174
+
175
+ logger.info("Fetching stories")
176
  stories = []
177
  try:
178
  for story in L.get_stories(userids=[profile.userid]):
 
184
  "timestamp": item.date_utc.isoformat(),
185
  "views": item.view_count if item.is_video else None,
186
  })
187
+ except instaloader.exceptions.QueryReturnedNotFoundException:
188
+ logger.error("Stories not found")
 
189
  raise HTTPException(
190
  status.HTTP_404_NOT_FOUND,
191
+ "Stories not available"
192
  )
193
 
194
  if not stories:
195
+ logger.info("No active stories found")
196
  raise HTTPException(
197
  status.HTTP_404_NOT_FOUND,
198
  "No active stories available"
199
  )
200
 
201
+ # Final randomized delay
202
+ time.sleep(random.uniform(0.5, 1.5))
203
+
204
+ logger.info(f"Returning {len(stories)} stories")
205
  return {"data": stories}
206
 
207
  except Exception as e:
208
+ logger.error(f"Critical failure: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  raise
210
 
211
+ # Keep the download endpoint from previous version