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.
Files changed (39) hide show
  1. package/bin/superbrain.js +196 -0
  2. package/package.json +23 -0
  3. package/payload/.dockerignore +45 -0
  4. package/payload/.env.example +58 -0
  5. package/payload/Dockerfile +73 -0
  6. package/payload/analyzers/__init__.py +0 -0
  7. package/payload/analyzers/audio_transcribe.py +225 -0
  8. package/payload/analyzers/caption.py +244 -0
  9. package/payload/analyzers/music_identifier.py +346 -0
  10. package/payload/analyzers/text_analyzer.py +117 -0
  11. package/payload/analyzers/visual_analyze.py +218 -0
  12. package/payload/analyzers/webpage_analyzer.py +789 -0
  13. package/payload/analyzers/youtube_analyzer.py +320 -0
  14. package/payload/api.py +1676 -0
  15. package/payload/config/.api_keys.example +22 -0
  16. package/payload/config/model_rankings.json +492 -0
  17. package/payload/config/openrouter_free_models.json +1364 -0
  18. package/payload/config/whisper_model.txt +1 -0
  19. package/payload/config_settings.py +185 -0
  20. package/payload/core/__init__.py +0 -0
  21. package/payload/core/category_manager.py +219 -0
  22. package/payload/core/database.py +811 -0
  23. package/payload/core/link_checker.py +300 -0
  24. package/payload/core/model_router.py +1253 -0
  25. package/payload/docker-compose.yml +120 -0
  26. package/payload/instagram/__init__.py +0 -0
  27. package/payload/instagram/instagram_downloader.py +253 -0
  28. package/payload/instagram/instagram_login.py +190 -0
  29. package/payload/main.py +912 -0
  30. package/payload/requirements.txt +39 -0
  31. package/payload/reset.py +311 -0
  32. package/payload/start-docker-prod.sh +125 -0
  33. package/payload/start-docker.sh +56 -0
  34. package/payload/start.py +1302 -0
  35. package/payload/static/favicon.ico +0 -0
  36. package/payload/stop-docker.sh +16 -0
  37. package/payload/utils/__init__.py +0 -0
  38. package/payload/utils/db_stats.py +108 -0
  39. 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()