Chrunos commited on
Commit
1b2c9e9
·
verified ·
1 Parent(s): a2c8032

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -76
app.py CHANGED
@@ -4,42 +4,74 @@ import instaloader
4
  import requests
5
  import os
6
  import time
 
7
  from functools import wraps
 
 
 
 
 
 
 
 
8
 
9
  app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
10
 
11
- # Load Instagram credentials from environment variables
12
  INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
13
  INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
14
 
15
- # Configure Instaloader with session persistence
16
- def get_instaloader():
17
- L = instaloader.Instaloader()
 
 
 
 
 
 
 
 
18
 
19
- # Try to load existing session
 
 
 
 
 
 
20
  try:
21
- L.load_session_from_file(INSTAGRAM_USERNAME)
22
- except FileNotFoundError:
23
- # If session doesn't exist, perform login
24
- try:
 
25
  L.login(INSTAGRAM_USERNAME, INSTAGRAM_PASSWORD)
26
- L.save_session_to_file()
27
- except instaloader.exceptions.BadCredentialsException:
28
- raise HTTPException(
29
- status_code=status.HTTP_401_UNAUTHORIZED,
30
- detail="Invalid Instagram credentials"
31
- )
32
- except instaloader.exceptions.TwoFactorAuthRequiredException:
33
- raise HTTPException(
34
- status_code=status.HTTP_401_UNAUTHORIZED,
35
- detail="Two-factor authentication required"
36
- )
37
-
38
- # Set request delay to avoid rate limits
39
- L.request_timeout = 300
40
- L.sleep = True
41
- L.delay_seconds = 5 # Minimum 5 seconds between requests
42
-
 
 
 
 
 
 
 
 
43
  return L
44
 
45
  def handle_instagram_errors(func):
@@ -47,81 +79,104 @@ def handle_instagram_errors(func):
47
  async def wrapper(*args, **kwargs):
48
  try:
49
  return await func(*args, **kwargs)
50
- except instaloader.exceptions.QueryReturnedBadRequestException:
 
51
  raise HTTPException(
52
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
53
- detail="Instagram rate limit exceeded - try again later"
54
  )
55
- except instaloader.exceptions.ConnectionException:
 
56
  raise HTTPException(
57
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
58
- detail="Instagram connection failed - try again later"
 
 
 
 
 
 
59
  )
60
- except instaloader.exceptions.PrivateProfileNotFollowedException:
 
61
  raise HTTPException(
62
- status_code=status.HTTP_403_FORBIDDEN,
63
- detail="Cannot access private profile"
64
  )
65
  return wrapper
66
 
67
  @app.get("/stories/{username}")
68
  @handle_instagram_errors
69
  async def get_stories(username: str):
70
- """Retrieve metadata for Instagram stories (public or private if followed)"""
71
- L = get_instaloader()
72
 
73
  try:
 
 
 
 
74
  profile = instaloader.Profile.from_username(L.context, username)
75
- except instaloader.exceptions.ProfileNotExistsException:
76
- raise HTTPException(status.HTTP_404_NOT_FOUND, "Profile not found")
77
-
78
- # Check if we can access stories
79
- if not profile.has_public_story and not profile.is_followed_by_viewer:
80
- raise HTTPException(
81
- status.HTTP_403_FORBIDDEN,
82
- "No accessible stories for this profile"
83
- )
84
 
85
- stories = []
86
- try:
87
- for story in L.get_stories(userids=[profile.userid]):
88
- for item in story.get_items():
89
- stories.append({
90
- "id": str(item.mediaid),
91
- "url": item.url,
92
- "type": "video" if item.is_video else "image",
93
- "timestamp": item.date_utc.isoformat(),
94
- "views": item.view_count if item.is_video else None,
95
- })
96
- except instaloader.exceptions.QueryReturnedNotFoundException:
97
- raise HTTPException(
98
- status.HTTP_404_NOT_FOUND,
99
- "Stories not found or expired"
100
- )
101
 
102
- if not stories:
103
- raise HTTPException(
104
- status.HTTP_404_NOT_FOUND,
105
- "No active stories available"
106
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- return {"data": stories}
 
 
 
 
 
109
 
110
  @app.get("/download/{media_id}")
111
  @handle_instagram_errors
112
  async def download_story(media_id: str, username: str):
113
- """Download specific story media by ID"""
114
- L = get_instaloader()
115
 
 
116
  try:
117
  profile = instaloader.Profile.from_username(L.context, username)
118
- except instaloader.exceptions.ProfileNotExistsException:
119
- raise HTTPException(status.HTTP_404_NOT_FOUND, "Profile not found")
 
120
 
121
- # Find the story item with retry logic
122
  story_item = None
123
- for _ in range(3): # Retry up to 3 times
124
  try:
 
125
  story_item = next(
126
  (item for story in L.get_stories(userids=[profile.userid])
127
  for item in story.get_items()
@@ -130,16 +185,20 @@ async def download_story(media_id: str, username: str):
130
  if story_item:
131
  break
132
  except instaloader.exceptions.QueryReturnedBadRequestException:
133
- time.sleep(10) # Wait 10 seconds before retrying
134
-
 
 
135
  if not story_item:
 
136
  raise HTTPException(status.HTTP_404_NOT_FOUND, "Story not found")
137
 
138
  try:
139
- # Use Instaloader's context to maintain session cookies
140
  response = L.context.get(story_item.url, stream=True)
141
  response.raise_for_status()
142
- except requests.exceptions.RequestException:
 
143
  raise HTTPException(
144
  status.HTTP_500_INTERNAL_SERVER_ERROR,
145
  "Failed to fetch media content"
 
4
  import requests
5
  import os
6
  import time
7
+ import logging
8
  from functools import wraps
9
+ from typing import Optional
10
+
11
+ # Configure logging
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15
+ )
16
+ logger = logging.getLogger(__name__)
17
 
18
  app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
19
 
20
+ # Environment variables
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):
 
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))
102
  raise HTTPException(
103
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
104
+ detail="Internal server error"
105
  )
106
  return wrapper
107
 
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]):
133
+ for item in story.get_items():
134
+ stories.append({
135
+ "id": str(item.mediaid),
136
+ "url": item.url,
137
+ "type": "video" if item.is_video else "image",
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()
 
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"