superbrain-server 1.0.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/superbrain.js +196 -0
- package/package.json +23 -0
- package/payload/.dockerignore +45 -0
- package/payload/.env.example +58 -0
- package/payload/Dockerfile +73 -0
- package/payload/analyzers/__init__.py +0 -0
- package/payload/analyzers/audio_transcribe.py +225 -0
- package/payload/analyzers/caption.py +244 -0
- package/payload/analyzers/music_identifier.py +346 -0
- package/payload/analyzers/text_analyzer.py +117 -0
- package/payload/analyzers/visual_analyze.py +218 -0
- package/payload/analyzers/webpage_analyzer.py +789 -0
- package/payload/analyzers/youtube_analyzer.py +320 -0
- package/payload/api.py +1676 -0
- package/payload/config/.api_keys.example +22 -0
- package/payload/config/model_rankings.json +492 -0
- package/payload/config/openrouter_free_models.json +1364 -0
- package/payload/config/whisper_model.txt +1 -0
- package/payload/config_settings.py +185 -0
- package/payload/core/__init__.py +0 -0
- package/payload/core/category_manager.py +219 -0
- package/payload/core/database.py +811 -0
- package/payload/core/link_checker.py +300 -0
- package/payload/core/model_router.py +1253 -0
- package/payload/docker-compose.yml +120 -0
- package/payload/instagram/__init__.py +0 -0
- package/payload/instagram/instagram_downloader.py +253 -0
- package/payload/instagram/instagram_login.py +190 -0
- package/payload/main.py +912 -0
- package/payload/requirements.txt +39 -0
- package/payload/reset.py +311 -0
- package/payload/start-docker-prod.sh +125 -0
- package/payload/start-docker.sh +56 -0
- package/payload/start.py +1302 -0
- package/payload/static/favicon.ico +0 -0
- package/payload/stop-docker.sh +16 -0
- package/payload/utils/__init__.py +0 -0
- package/payload/utils/db_stats.py +108 -0
- package/payload/utils/manage_token.py +91 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# SuperBrain Production Docker Compose Configuration
|
|
3
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
|
|
7
|
+
# ──── Main SuperBrain API Service ────
|
|
8
|
+
superbrain-api:
|
|
9
|
+
image: superbrain:latest
|
|
10
|
+
build:
|
|
11
|
+
context: .
|
|
12
|
+
dockerfile: Dockerfile
|
|
13
|
+
container_name: superbrain-api
|
|
14
|
+
|
|
15
|
+
# ──── Network & Ports ────
|
|
16
|
+
ports:
|
|
17
|
+
- "${API_PORT:-5000}:5000"
|
|
18
|
+
networks:
|
|
19
|
+
- superbrain-network
|
|
20
|
+
|
|
21
|
+
# ──── Environment Variables ────
|
|
22
|
+
environment:
|
|
23
|
+
PYTHONUNBUFFERED: 1
|
|
24
|
+
PYTHONDONTWRITEBYTECODE: 1
|
|
25
|
+
ENVIRONMENT: ${ENVIRONMENT:-production}
|
|
26
|
+
HOST: 0.0.0.0
|
|
27
|
+
PORT: 5000
|
|
28
|
+
WORKERS: ${WORKERS:-4}
|
|
29
|
+
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
30
|
+
|
|
31
|
+
# Database
|
|
32
|
+
DATABASE_PATH: /app/data/superbrain.db
|
|
33
|
+
DATABASE_TIMEOUT: 30
|
|
34
|
+
|
|
35
|
+
# API Keys (load from .env file)
|
|
36
|
+
GROQ_API_KEY: ${GROQ_API_KEY}
|
|
37
|
+
GEMINI_API_KEY: ${GEMINI_API_KEY}
|
|
38
|
+
GOOGLE_API_KEY: ${GOOGLE_API_KEY}
|
|
39
|
+
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
|
|
40
|
+
|
|
41
|
+
# Instagram credentials
|
|
42
|
+
INSTAGRAM_USERNAME: ${INSTAGRAM_USERNAME}
|
|
43
|
+
INSTAGRAM_PASSWORD: ${INSTAGRAM_PASSWORD}
|
|
44
|
+
|
|
45
|
+
# AI Configuration
|
|
46
|
+
WHISPER_MODEL: ${WHISPER_MODEL:-base}
|
|
47
|
+
WHISPER_USE_CLOUD: ${WHISPER_USE_CLOUD:-true}
|
|
48
|
+
|
|
49
|
+
# Performance
|
|
50
|
+
MAX_UPLOAD_SIZE: ${MAX_UPLOAD_SIZE:-52428800}
|
|
51
|
+
REQUEST_TIMEOUT: ${REQUEST_TIMEOUT:-60}
|
|
52
|
+
ANALYSIS_TIMEOUT: ${ANALYSIS_TIMEOUT:-120}
|
|
53
|
+
|
|
54
|
+
# ──── Volumes ────
|
|
55
|
+
volumes:
|
|
56
|
+
# Database persistence
|
|
57
|
+
- superbrain-db:/app/data
|
|
58
|
+
# Configuration persistence
|
|
59
|
+
- superbrain-config:/app/config
|
|
60
|
+
# Temporary files
|
|
61
|
+
- superbrain-temp:/app/temp
|
|
62
|
+
# Uploads
|
|
63
|
+
- superbrain-uploads:/app/static/uploads
|
|
64
|
+
# Logs
|
|
65
|
+
- superbrain-logs:/app/logs
|
|
66
|
+
|
|
67
|
+
# ──── Restart Policy ────
|
|
68
|
+
restart: unless-stopped
|
|
69
|
+
|
|
70
|
+
# ──── Health Check ────
|
|
71
|
+
healthcheck:
|
|
72
|
+
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
|
73
|
+
interval: 30s
|
|
74
|
+
timeout: 10s
|
|
75
|
+
retries: 3
|
|
76
|
+
start_period: 40s
|
|
77
|
+
|
|
78
|
+
# ──── Resource Limits (optional, enable for production) ────
|
|
79
|
+
# uncomment to enable
|
|
80
|
+
# deploy:
|
|
81
|
+
# resources:
|
|
82
|
+
# limits:
|
|
83
|
+
# cpus: '2'
|
|
84
|
+
# memory: 2G
|
|
85
|
+
# reservations:
|
|
86
|
+
# cpus: '1'
|
|
87
|
+
# memory: 1G
|
|
88
|
+
|
|
89
|
+
# ──── Security Options ────
|
|
90
|
+
security_opt:
|
|
91
|
+
- no-new-privileges:true
|
|
92
|
+
read_only: false
|
|
93
|
+
cap_drop:
|
|
94
|
+
- ALL
|
|
95
|
+
cap_add:
|
|
96
|
+
- NET_BIND_SERVICE
|
|
97
|
+
|
|
98
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
# Networks
|
|
100
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
networks:
|
|
103
|
+
superbrain-network:
|
|
104
|
+
driver: bridge
|
|
105
|
+
|
|
106
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
# Volumes for data persistence
|
|
108
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
volumes:
|
|
111
|
+
superbrain-db:
|
|
112
|
+
driver: local
|
|
113
|
+
superbrain-config:
|
|
114
|
+
driver: local
|
|
115
|
+
superbrain-temp:
|
|
116
|
+
driver: local
|
|
117
|
+
superbrain-uploads:
|
|
118
|
+
driver: local
|
|
119
|
+
superbrain-logs:
|
|
120
|
+
driver: local
|
|
File without changes
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Instagram post downloader — instaloader engine.
|
|
3
|
+
|
|
4
|
+
Uses an authenticated instaloader session if one exists
|
|
5
|
+
(created by instagram_login.py), falls back to anonymous otherwise.
|
|
6
|
+
|
|
7
|
+
Session file : backend/.instaloader_session (gitignored)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import pathlib
|
|
14
|
+
import contextlib
|
|
15
|
+
|
|
16
|
+
# Ensure backend root is in sys.path (needed when run as a subprocess)
|
|
17
|
+
import sys as _sys_
|
|
18
|
+
_sys_.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
|
19
|
+
del _sys_
|
|
20
|
+
from urllib.request import urlretrieve as _urlretrieve_il
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RetryableDownloadError(Exception):
|
|
24
|
+
"""Raised when Instagram download fails due to a transient issue
|
|
25
|
+
(rate limit / login required) that should be retried later."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
# ── instaloader ───────────────────────────────────────────────────────────────
|
|
29
|
+
try:
|
|
30
|
+
import instaloader
|
|
31
|
+
INSTALOADER_AVAILABLE = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
INSTALOADER_AVAILABLE = False
|
|
34
|
+
|
|
35
|
+
# ── Audio extraction ──────────────────────────────────────────────────────────
|
|
36
|
+
try:
|
|
37
|
+
from moviepy.editor import VideoFileClip
|
|
38
|
+
MOVIEPY_AVAILABLE = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
MOVIEPY_AVAILABLE = False
|
|
41
|
+
|
|
42
|
+
# ── Paths ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
BACKEND_DIR = pathlib.Path(__file__).parent.parent
|
|
44
|
+
TEMP_DIR = BACKEND_DIR / "temp"
|
|
45
|
+
IL_SESSION_FILE = BACKEND_DIR / ".instaloader_session"
|
|
46
|
+
API_KEYS_FILE = BACKEND_DIR / "config" / ".api_keys"
|
|
47
|
+
LEGACY_API_KEYS_FILE = BACKEND_DIR / ".api_keys"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Credential loader ─────────────────────────────────────────────────────────
|
|
51
|
+
def _load_credentials() -> tuple[str, str]:
|
|
52
|
+
"""Read INSTAGRAM_USERNAME / INSTAGRAM_PASSWORD from .api_keys or env."""
|
|
53
|
+
creds: dict[str, str] = {}
|
|
54
|
+
key_file = API_KEYS_FILE if API_KEYS_FILE.exists() else LEGACY_API_KEYS_FILE
|
|
55
|
+
if key_file.exists():
|
|
56
|
+
for line in key_file.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
57
|
+
line = line.strip()
|
|
58
|
+
if "=" in line and not line.startswith("#"):
|
|
59
|
+
k, _, v = line.partition("=")
|
|
60
|
+
creds[k.strip()] = v.strip()
|
|
61
|
+
|
|
62
|
+
username = creds.get("INSTAGRAM_USERNAME") or os.getenv("INSTAGRAM_USERNAME", "")
|
|
63
|
+
password = creds.get("INSTAGRAM_PASSWORD") or os.getenv("INSTAGRAM_PASSWORD", "")
|
|
64
|
+
return username, password
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
68
|
+
def sanitize_folder_name(text: str, max_length: int = 50) -> str:
|
|
69
|
+
"""Strip non-ASCII and filesystem-unsafe characters; trim to max_length."""
|
|
70
|
+
text = text.encode("ascii", "ignore").decode("ascii")
|
|
71
|
+
text = re.sub(r'[<>:"/\\|?*\n\r]', "", text)
|
|
72
|
+
text = re.sub(r"\s+", " ", text).strip()[:max_length]
|
|
73
|
+
return text or "instagram_post"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _unique_folder(base: pathlib.Path, name: str) -> pathlib.Path:
|
|
77
|
+
"""Return a non-existing path under base, appending _N suffix if needed."""
|
|
78
|
+
folder = base / name
|
|
79
|
+
counter = 1
|
|
80
|
+
while folder.exists():
|
|
81
|
+
folder = base / f"{name}_{counter}"
|
|
82
|
+
counter += 1
|
|
83
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
return folder
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def extract_audio_from_video(video_path: str, audio_path: str) -> bool:
|
|
88
|
+
"""Extract audio track from a video file and save as MP3."""
|
|
89
|
+
if not MOVIEPY_AVAILABLE:
|
|
90
|
+
print(" ⚠ moviepy not installed — skipping audio extraction")
|
|
91
|
+
return False
|
|
92
|
+
try:
|
|
93
|
+
print(" Extracting audio...")
|
|
94
|
+
video = VideoFileClip(video_path)
|
|
95
|
+
if video.audio is not None:
|
|
96
|
+
video.audio.write_audiofile(audio_path, verbose=False, logger=None)
|
|
97
|
+
video.close()
|
|
98
|
+
print(f" ✓ Audio saved: {os.path.basename(audio_path)}")
|
|
99
|
+
return True
|
|
100
|
+
video.close()
|
|
101
|
+
print(" ⚠ No audio track in video")
|
|
102
|
+
return False
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f" ⚠ Audio extraction failed: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── Main download function ────────────────────────────────────────────────────
|
|
109
|
+
def download_instagram_content(url: str) -> str | None:
|
|
110
|
+
"""
|
|
111
|
+
Download an Instagram post (photo / video / carousel) to temp/<folder>/.
|
|
112
|
+
|
|
113
|
+
Uses authenticated instaloader session if available, anonymous otherwise.
|
|
114
|
+
Returns the folder path string on success, or None on failure.
|
|
115
|
+
"""
|
|
116
|
+
TEMP_DIR.mkdir(exist_ok=True)
|
|
117
|
+
|
|
118
|
+
if INSTALOADER_AVAILABLE:
|
|
119
|
+
return _download_via_instaloader(url)
|
|
120
|
+
|
|
121
|
+
print("✗ instaloader is not installed. Run: pip install instaloader")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── instaloader engine ───────────────────────────────────────────────────────
|
|
127
|
+
def _download_via_instaloader(url: str) -> str | None:
|
|
128
|
+
"""Download using instaloader — authenticated session if available, else anonymous."""
|
|
129
|
+
match = re.search(r"/(?:reels?|p|tv)/([^/?#&]+)", url)
|
|
130
|
+
if not match:
|
|
131
|
+
print(" ✗ Invalid Instagram URL.")
|
|
132
|
+
return None
|
|
133
|
+
shortcode = match.group(1)
|
|
134
|
+
|
|
135
|
+
L = instaloader.Instaloader(
|
|
136
|
+
download_video_thumbnails=False,
|
|
137
|
+
download_geotags=False,
|
|
138
|
+
download_comments=False,
|
|
139
|
+
save_metadata=False,
|
|
140
|
+
compress_json=False,
|
|
141
|
+
iphone_support=False, # disable mobile API — use web only, no 403s
|
|
142
|
+
user_agent=(
|
|
143
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
144
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
145
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
146
|
+
),
|
|
147
|
+
max_connection_attempts=3,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Load saved instaloader session if one exists (set up by instagram_login.py)
|
|
151
|
+
username, _ = _load_credentials()
|
|
152
|
+
if not username and not IL_SESSION_FILE.exists():
|
|
153
|
+
print(" ℹ No Instagram credentials/session found — anonymous mode may fail for some posts.")
|
|
154
|
+
if IL_SESSION_FILE.exists() and username:
|
|
155
|
+
try:
|
|
156
|
+
L.load_session_from_file(username, str(IL_SESSION_FILE))
|
|
157
|
+
print(f" ✓ Using authenticated instaloader session (@{username})")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f" ⚠ Could not load instaloader session ({e}) — using anonymous")
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
print(f" Fetching post for shortcode: {shortcode}...")
|
|
163
|
+
with contextlib.redirect_stderr(open(os.devnull, "w")):
|
|
164
|
+
post = instaloader.Post.from_shortcode(L.context, shortcode)
|
|
165
|
+
|
|
166
|
+
caption = post.caption if post.caption else f"post_{shortcode}"
|
|
167
|
+
caption_first_line = caption.split("\n")[0]
|
|
168
|
+
folder_name = sanitize_folder_name(caption_first_line)
|
|
169
|
+
folder = _unique_folder(TEMP_DIR, folder_name)
|
|
170
|
+
|
|
171
|
+
print(f" User : @{post.owner_username}")
|
|
172
|
+
print(f" Caption: {caption_first_line[:80]}...")
|
|
173
|
+
|
|
174
|
+
file_counter = 1
|
|
175
|
+
if post.is_video:
|
|
176
|
+
video_path = str(folder / f"{folder_name}.mp4")
|
|
177
|
+
print(" Downloading video...")
|
|
178
|
+
_urlretrieve_il(post.video_url, video_path)
|
|
179
|
+
extract_audio_from_video(video_path,
|
|
180
|
+
str(folder / f"{folder_name}_audio.mp3"))
|
|
181
|
+
_urlretrieve_il(post.url, str(folder / f"{folder_name}_thumbnail.jpg"))
|
|
182
|
+
|
|
183
|
+
elif post.typename == "GraphSidecar":
|
|
184
|
+
print(f" Downloading carousel ({post.mediacount} items)...")
|
|
185
|
+
for node in post.get_sidecar_nodes():
|
|
186
|
+
if node.is_video:
|
|
187
|
+
fp = str(folder / f"{folder_name}_{file_counter}.mp4")
|
|
188
|
+
_urlretrieve_il(node.video_url, fp)
|
|
189
|
+
extract_audio_from_video(
|
|
190
|
+
fp,
|
|
191
|
+
str(folder / f"{folder_name}_{file_counter}_audio.mp3")
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
_urlretrieve_il(
|
|
195
|
+
node.display_url,
|
|
196
|
+
str(folder / f"{folder_name}_{file_counter}.jpg")
|
|
197
|
+
)
|
|
198
|
+
print(f" → item {file_counter}/{post.mediacount}")
|
|
199
|
+
file_counter += 1
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
print(" Downloading image...")
|
|
203
|
+
_urlretrieve_il(post.url, str(folder / f"{folder_name}.jpg"))
|
|
204
|
+
|
|
205
|
+
# info.txt (instaloader variant — uses post object fields)
|
|
206
|
+
with open(folder / "info.txt", "w", encoding="utf-8") as f:
|
|
207
|
+
f.write("Instagram Post Information\n")
|
|
208
|
+
f.write("=" * 50 + "\n\n")
|
|
209
|
+
f.write(f"URL: {url}\n")
|
|
210
|
+
f.write(f"Username: @{post.owner_username}\n")
|
|
211
|
+
f.write(f"Date: {post.date_utc}\n")
|
|
212
|
+
f.write(f"Likes: {post.likes}\n")
|
|
213
|
+
f.write(f"Type: {'Video' if post.is_video else 'Image'}\n")
|
|
214
|
+
if post.typename == "GraphSidecar":
|
|
215
|
+
f.write(f"Media Count: {post.mediacount} items\n")
|
|
216
|
+
f.write("\n")
|
|
217
|
+
f.write(f"Caption:\n{'-' * 50}\n")
|
|
218
|
+
f.write(post.caption if post.caption else "No caption")
|
|
219
|
+
f.write(f"\n{'-' * 50}\n\n")
|
|
220
|
+
hashtags = re.findall(r"#\w+", post.caption or "")
|
|
221
|
+
if hashtags:
|
|
222
|
+
f.write("Hashtags:\n")
|
|
223
|
+
f.write(", ".join(hashtags) + "\n")
|
|
224
|
+
|
|
225
|
+
print(f"\n✓ Download complete (instaloader): {folder}")
|
|
226
|
+
return str(folder)
|
|
227
|
+
|
|
228
|
+
except instaloader.exceptions.LoginRequiredException:
|
|
229
|
+
raise RetryableDownloadError(
|
|
230
|
+
"Login required — Instagram blocked anonymous access. Will retry later."
|
|
231
|
+
)
|
|
232
|
+
except instaloader.exceptions.ConnectionException as e:
|
|
233
|
+
_msg = str(e).lower()
|
|
234
|
+
if any(k in _msg for k in ("too many", "rate", "wait", "429", "blocked", "checkpoint")):
|
|
235
|
+
raise RetryableDownloadError(f"Instagram rate-limited: {e}")
|
|
236
|
+
print(f" ✗ instaloader: Connection error — {e}")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f" ✗ instaloader error: {e}")
|
|
239
|
+
import traceback; traceback.print_exc()
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ── CLI entry-point ───────────────────────────────────────────────────────────
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
if len(sys.argv) > 1:
|
|
246
|
+
_url = sys.argv[1]
|
|
247
|
+
else:
|
|
248
|
+
_url = input("Enter Instagram Reel / Post / IGTV link: ").strip()
|
|
249
|
+
|
|
250
|
+
if _url:
|
|
251
|
+
download_instagram_content(_url)
|
|
252
|
+
else:
|
|
253
|
+
print("No URL provided.")
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Instagram Session Setup Script
|
|
4
|
+
===============================
|
|
5
|
+
Run this ONCE to authenticate with Instagram and save an instaloader session.
|
|
6
|
+
After that, instagram_downloader.py will reuse the saved session automatically.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python instagram_login.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import pathlib
|
|
15
|
+
|
|
16
|
+
# Ensure backend root is in sys.path (needed when run directly)
|
|
17
|
+
import sys as _sys_
|
|
18
|
+
_sys_.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
|
19
|
+
del _sys_
|
|
20
|
+
|
|
21
|
+
# ─── Paths ────────────────────────────────────────────────────────────────────
|
|
22
|
+
BACKEND_DIR = pathlib.Path(__file__).parent.parent
|
|
23
|
+
API_KEYS_FILE = BACKEND_DIR / ".api_keys"
|
|
24
|
+
IL_SESSION_FILE = BACKEND_DIR / ".instaloader_session"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _banner(msg: str) -> None:
|
|
28
|
+
print("\n" + "=" * 60)
|
|
29
|
+
print(f" {msg}")
|
|
30
|
+
print("=" * 60)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_credentials() -> tuple[str, str]:
|
|
34
|
+
creds: dict[str, str] = {}
|
|
35
|
+
if API_KEYS_FILE.exists():
|
|
36
|
+
for line in API_KEYS_FILE.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
37
|
+
line = line.strip()
|
|
38
|
+
if "=" in line and not line.startswith("#"):
|
|
39
|
+
k, _, v = line.partition("=")
|
|
40
|
+
creds[k.strip()] = v.strip()
|
|
41
|
+
username = creds.get("INSTAGRAM_USERNAME") or os.getenv("INSTAGRAM_USERNAME", "")
|
|
42
|
+
password = creds.get("INSTAGRAM_PASSWORD") or os.getenv("INSTAGRAM_PASSWORD", "")
|
|
43
|
+
return username, password
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _save_credentials(username: str, password: str) -> None:
|
|
47
|
+
lines: list[str] = []
|
|
48
|
+
if API_KEYS_FILE.exists():
|
|
49
|
+
for line in API_KEYS_FILE.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
50
|
+
stripped = line.strip()
|
|
51
|
+
if "=" in stripped and not stripped.startswith("#"):
|
|
52
|
+
k = stripped.split("=", 1)[0].strip()
|
|
53
|
+
if k not in ("INSTAGRAM_USERNAME", "INSTAGRAM_PASSWORD"):
|
|
54
|
+
lines.append(line)
|
|
55
|
+
else:
|
|
56
|
+
lines.append(line)
|
|
57
|
+
lines.append(f"INSTAGRAM_USERNAME={username}")
|
|
58
|
+
lines.append(f"INSTAGRAM_PASSWORD={password}")
|
|
59
|
+
API_KEYS_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
60
|
+
print(f"✓ Credentials saved to {API_KEYS_FILE.name}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
# PART 1 — instaloader session (web API, works on any IP)
|
|
65
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
def setup_instaloader_session(username: str, password: str) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Log in with instaloader (Instagram Web API), save session to a file.
|
|
70
|
+
Handles 2FA interactively.
|
|
71
|
+
Returns True on success.
|
|
72
|
+
"""
|
|
73
|
+
_banner("📥 Setting up instaloader session (web API)")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
import instaloader
|
|
77
|
+
except ImportError:
|
|
78
|
+
print("✗ instaloader not installed. Run: pip install instaloader")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
L = instaloader.Instaloader(
|
|
82
|
+
download_video_thumbnails=False,
|
|
83
|
+
download_geotags=False,
|
|
84
|
+
download_comments=False,
|
|
85
|
+
save_metadata=False,
|
|
86
|
+
compress_json=False,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Try reusing existing session
|
|
90
|
+
if IL_SESSION_FILE.exists():
|
|
91
|
+
try:
|
|
92
|
+
L.load_session_from_file(username, str(IL_SESSION_FILE))
|
|
93
|
+
if L.context.is_logged_in:
|
|
94
|
+
print(f"✓ Reused saved instaloader session for @{username}")
|
|
95
|
+
return True
|
|
96
|
+
except Exception:
|
|
97
|
+
print(" Existing instaloader session invalid — re-logging in.")
|
|
98
|
+
IL_SESSION_FILE.unlink(missing_ok=True)
|
|
99
|
+
|
|
100
|
+
# Fresh login
|
|
101
|
+
print(f" Logging in as @{username} ...")
|
|
102
|
+
try:
|
|
103
|
+
L.login(username, password)
|
|
104
|
+
except instaloader.exceptions.TwoFactorAuthRequiredException:
|
|
105
|
+
print("\n 📱 Two-factor authentication required.")
|
|
106
|
+
print(" Check your authenticator app or SMS for the 6-digit code.\n")
|
|
107
|
+
while True:
|
|
108
|
+
code = input(" Enter 2FA code: ").strip().replace(" ", "")
|
|
109
|
+
if len(code) == 6 and code.isdigit():
|
|
110
|
+
break
|
|
111
|
+
print(" Code should be 6 digits.")
|
|
112
|
+
try:
|
|
113
|
+
L.two_factor_login(code)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f" ✗ 2FA failed: {e}")
|
|
116
|
+
return False
|
|
117
|
+
except instaloader.exceptions.BadCredentialsException:
|
|
118
|
+
print(" ✗ Incorrect username or password.")
|
|
119
|
+
return False
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f" ✗ Login error: {e}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
if not L.context.is_logged_in:
|
|
125
|
+
print(" ✗ Login did not succeed.")
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
# Save session
|
|
129
|
+
L.save_session_to_file(str(IL_SESSION_FILE))
|
|
130
|
+
print(f"✓ instaloader session saved → {IL_SESSION_FILE.name}")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
137
|
+
# Main
|
|
138
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
def main() -> None:
|
|
141
|
+
_banner("SuperBrain — Instagram Session Setup")
|
|
142
|
+
print()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
import instaloader # noqa: F401
|
|
146
|
+
except ImportError:
|
|
147
|
+
print("✗ instaloader is not installed. Run: pip install instaloader")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
username, password = _load_credentials()
|
|
151
|
+
|
|
152
|
+
if username and password:
|
|
153
|
+
print(f" Found credentials for: @{username}")
|
|
154
|
+
answer = input(" Use these? [Y/n]: ").strip().lower()
|
|
155
|
+
if answer == "n":
|
|
156
|
+
username = ""
|
|
157
|
+
password = ""
|
|
158
|
+
|
|
159
|
+
if not username:
|
|
160
|
+
username = input("\n Instagram username: ").strip().lstrip("@")
|
|
161
|
+
if not password:
|
|
162
|
+
import getpass
|
|
163
|
+
password = getpass.getpass(" Instagram password: ")
|
|
164
|
+
|
|
165
|
+
if not username or not password:
|
|
166
|
+
print("✗ Username and password are required.")
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
il_ok = setup_instaloader_session(username, password)
|
|
170
|
+
|
|
171
|
+
# Save credentials if entered interactively
|
|
172
|
+
existing_user, _ = _load_credentials()
|
|
173
|
+
if not existing_user:
|
|
174
|
+
_save_credentials(username, password)
|
|
175
|
+
|
|
176
|
+
_banner("Summary")
|
|
177
|
+
il_status = "\u2713 saved" if il_ok else "\u2717 not saved"
|
|
178
|
+
print(f" instaloader session : {il_status}")
|
|
179
|
+
print()
|
|
180
|
+
if il_ok:
|
|
181
|
+
print(" SuperBrain will now use the authenticated instaloader session")
|
|
182
|
+
print(" for all Instagram downloads.")
|
|
183
|
+
print()
|
|
184
|
+
print(" Re-run this script if Instagram ever asks you to log in again.")
|
|
185
|
+
else:
|
|
186
|
+
print(" ⚠ No session saved. Anonymous instaloader will be used.")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main()
|