Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException, status
|
|
|
|
| 2 |
import instaloader
|
|
|
|
| 3 |
import os
|
| 4 |
import time
|
| 5 |
import logging
|
|
@@ -169,13 +171,26 @@ async def get_stories(username: str):
|
|
| 169 |
try:
|
| 170 |
for story in L.get_stories(userids=[profile.userid]):
|
| 171 |
for item in story.get_items():
|
| 172 |
-
|
|
|
|
| 173 |
"id": str(item.mediaid),
|
| 174 |
"url": item.url,
|
| 175 |
"type": "video" if item.is_video else "image",
|
| 176 |
"timestamp": item.date_utc.isoformat(),
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
except instaloader.exceptions.QueryReturnedNotFoundException:
|
| 180 |
logger.error("Stories not found")
|
| 181 |
raise HTTPException(
|
|
@@ -198,4 +213,67 @@ async def get_stories(username: str):
|
|
| 198 |
|
| 199 |
except Exception as e:
|
| 200 |
logger.error(f"Critical failure: {str(e)}")
|
| 201 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException, status
|
| 2 |
+
from fastapi.responses import StreamingResponse
|
| 3 |
import instaloader
|
| 4 |
+
import requests
|
| 5 |
import os
|
| 6 |
import time
|
| 7 |
import logging
|
|
|
|
| 171 |
try:
|
| 172 |
for story in L.get_stories(userids=[profile.userid]):
|
| 173 |
for item in story.get_items():
|
| 174 |
+
# Create a story dict with safe attribute access
|
| 175 |
+
story_data = {
|
| 176 |
"id": str(item.mediaid),
|
| 177 |
"url": item.url,
|
| 178 |
"type": "video" if item.is_video else "image",
|
| 179 |
"timestamp": item.date_utc.isoformat(),
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# Only try to add view_count if it's a video and the attribute exists
|
| 183 |
+
if item.is_video:
|
| 184 |
+
try:
|
| 185 |
+
# Safely check if view_count attribute exists
|
| 186 |
+
if hasattr(item, 'view_count'):
|
| 187 |
+
story_data["views"] = item.view_count
|
| 188 |
+
except AttributeError:
|
| 189 |
+
# Skip adding views if the attribute doesn't exist
|
| 190 |
+
pass
|
| 191 |
+
|
| 192 |
+
stories.append(story_data)
|
| 193 |
+
|
| 194 |
except instaloader.exceptions.QueryReturnedNotFoundException:
|
| 195 |
logger.error("Stories not found")
|
| 196 |
raise HTTPException(
|
|
|
|
| 213 |
|
| 214 |
except Exception as e:
|
| 215 |
logger.error(f"Critical failure: {str(e)}")
|
| 216 |
+
raise
|
| 217 |
+
|
| 218 |
+
@app.get("/download/{url:path}")
|
| 219 |
+
@handle_instagram_errors
|
| 220 |
+
async def download_media(url: str):
|
| 221 |
+
"""Download and proxy media content"""
|
| 222 |
+
logger.info(f"Download request for: {url}")
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
# Basic validation to prevent arbitrary URL access
|
| 226 |
+
if not url.startswith(("https://instagram", "https://scontent")):
|
| 227 |
+
raise HTTPException(
|
| 228 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 229 |
+
detail="Invalid URL format"
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# Random delay to avoid detection patterns
|
| 233 |
+
time.sleep(random.uniform(1.0, 3.0))
|
| 234 |
+
|
| 235 |
+
# Configure headers to mimic a browser
|
| 236 |
+
headers = {
|
| 237 |
+
"User-Agent": random.choice(USER_AGENTS),
|
| 238 |
+
"Accept": "*/*",
|
| 239 |
+
"Accept-Language": "en-US,en;q=0.9",
|
| 240 |
+
"Referer": "https://www.instagram.com/",
|
| 241 |
+
"Sec-Fetch-Dest": "empty",
|
| 242 |
+
"Sec-Fetch-Mode": "cors",
|
| 243 |
+
"Sec-Fetch-Site": "cross-site",
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
# Request the media with a session
|
| 247 |
+
response = requests.get(url, headers=headers, stream=True, timeout=30)
|
| 248 |
+
response.raise_for_status()
|
| 249 |
+
|
| 250 |
+
# Determine content type from response or URL
|
| 251 |
+
if "Content-Type" in response.headers:
|
| 252 |
+
content_type = response.headers["Content-Type"]
|
| 253 |
+
elif url.endswith((".jpg", ".jpeg")):
|
| 254 |
+
content_type = "image/jpeg"
|
| 255 |
+
elif url.endswith(".mp4"):
|
| 256 |
+
content_type = "video/mp4"
|
| 257 |
+
else:
|
| 258 |
+
content_type = "application/octet-stream"
|
| 259 |
+
|
| 260 |
+
logger.info(f"Media downloaded successfully: {content_type}")
|
| 261 |
+
|
| 262 |
+
# Stream the response back
|
| 263 |
+
return StreamingResponse(
|
| 264 |
+
response.iter_content(chunk_size=8192),
|
| 265 |
+
media_type=content_type,
|
| 266 |
+
headers={
|
| 267 |
+
"Content-Disposition": f"attachment; filename={url.split('/')[-1]}",
|
| 268 |
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
| 269 |
+
"Pragma": "no-cache",
|
| 270 |
+
"Expires": "0",
|
| 271 |
+
}
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
except requests.exceptions.RequestException as e:
|
| 275 |
+
logger.error(f"Download failed: {str(e)}")
|
| 276 |
+
raise HTTPException(
|
| 277 |
+
status_code=status.HTTP_502_BAD_GATEWAY,
|
| 278 |
+
detail="Failed to download media"
|
| 279 |
+
)
|