superbrain-server 1.0.10 → 1.0.12

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 (37) hide show
  1. package/bin/superbrain.js +4 -0
  2. package/package.json +1 -1
  3. package/payload/__pycache__/api.cpython-311.pyc +0 -0
  4. package/payload/__pycache__/main.cpython-311.pyc +0 -0
  5. package/payload/__pycache__/start.cpython-311.pyc +0 -0
  6. package/payload/config/.api_keys +3 -0
  7. package/payload/start.py +82 -149
  8. package/payload/analyzers/__pycache__/__init__.cpython-311.pyc +0 -0
  9. package/payload/analyzers/__pycache__/audio_transcribe.cpython-311.pyc +0 -0
  10. package/payload/analyzers/__pycache__/caption.cpython-311.pyc +0 -0
  11. package/payload/analyzers/__pycache__/music_identifier.cpython-311.pyc +0 -0
  12. package/payload/analyzers/__pycache__/text_analyzer.cpython-311.pyc +0 -0
  13. package/payload/analyzers/__pycache__/visual_analyze.cpython-311.pyc +0 -0
  14. package/payload/analyzers/__pycache__/webpage_analyzer.cpython-311.pyc +0 -0
  15. package/payload/analyzers/__pycache__/youtube_analyzer.cpython-311.pyc +0 -0
  16. package/payload/config/backend_id.txt +0 -1
  17. package/payload/config/localtunnel.log +0 -86
  18. package/payload/core/__pycache__/__init__.cpython-311.pyc +0 -0
  19. package/payload/core/__pycache__/category_manager.cpython-311.pyc +0 -0
  20. package/payload/core/__pycache__/database.cpython-311.pyc +0 -0
  21. package/payload/core/__pycache__/link_checker.cpython-311.pyc +0 -0
  22. package/payload/core/__pycache__/model_router.cpython-311.pyc +0 -0
  23. package/payload/instagram/__pycache__/__init__.cpython-311.pyc +0 -0
  24. package/payload/instagram/__pycache__/instagram_downloader.cpython-311.pyc +0 -0
  25. package/payload/instagram/__pycache__/instagram_login.cpython-311.pyc +0 -0
  26. package/payload/test_backend.py +0 -241
  27. package/payload/tests/__init__.py +0 -0
  28. package/payload/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  29. package/payload/tests/__pycache__/test_api.cpython-311.pyc +0 -0
  30. package/payload/tests/__pycache__/test_db.cpython-311.pyc +0 -0
  31. package/payload/tests/__pycache__/test_sync_code.cpython-311.pyc +0 -0
  32. package/payload/tests/test_api.py +0 -17
  33. package/payload/tests/test_db.py +0 -22
  34. package/payload/tests/test_sync_code.py +0 -65
  35. package/payload/utils/__pycache__/__init__.cpython-311.pyc +0 -0
  36. package/payload/utils/__pycache__/db_stats.cpython-311.pyc +0 -0
  37. package/payload/utils/__pycache__/manage_token.cpython-311.pyc +0 -0
package/bin/superbrain.js CHANGED
@@ -171,11 +171,15 @@ if (userArgs.length > 0) {
171
171
  superbrain-server reset -> Open Reset Menu
172
172
  superbrain-server reset --all -> Force complete wipe
173
173
  superbrain-server status -> Show QR Code and Server Info
174
+ superbrain-server ngrok -> Configure Ngrok tunnel
174
175
  `);
175
176
  process.exit(0);
176
177
  } else if (cmd === 'status' || cmd === 'update') {
177
178
  targetScript = 'start.py';
178
179
  finalArgs = ['--status'];
180
+ } else if (cmd === 'ngrok') {
181
+ targetScript = 'start.py';
182
+ finalArgs = ['--ngrok'];
179
183
  }
180
184
  }
181
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbrain-server",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,3 @@
1
+ GROQ_API_KEY=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
2
+ GEMINI_API_KEY=AIzaSyAcr5QSifNG0XejGw3LbQ_jkOYLx4-p8fk
3
+ OPENROUTER_API_KEY=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
package/payload/start.py CHANGED
@@ -22,6 +22,7 @@ import string
22
22
  import textwrap
23
23
  import time
24
24
  import importlib
25
+ import re
25
26
  from pathlib import Path
26
27
 
27
28
  # ── Paths ─────────────────────────────────────────────────────────────────────
@@ -157,6 +158,7 @@ def ensure_runtime_dependencies():
157
158
  ("fastapi", "fastapi"),
158
159
  ("uvicorn", "uvicorn"),
159
160
  ("multipart", "python-multipart"),
161
+ ("pyngrok", "pyngrok"),
160
162
  ("instaloader", "instaloader"),
161
163
  ("segno", "segno"),
162
164
  ]
@@ -662,64 +664,41 @@ def setup_whisper():
662
664
  # ══════════════════════════════════════════════════════════════════════════════
663
665
  # Step 6 — Remote Access / Port Forwarding
664
666
  # ══════════════════════════════════════════════════════════════════════════════
665
- LOCALTUNNEL_ENABLED = BASE_DIR / "config" / "localtunnel_enabled.txt"
666
- LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
667
+ NGROK_ENABLED = BASE_DIR / "config" / "ngrok_enabled.txt"
668
+ NGROK_TOKEN = BASE_DIR / "config" / "ngrok_token.txt"
667
669
 
668
670
  def setup_remote_access():
669
- h1("Step 6 of 7 — Remote Access (localtunnel / Port Forwarding)")
671
+ h1("Step 6 of 7 — Remote Access (ngrok)")
670
672
 
671
673
  print(f"""
672
- The SuperBrain backend runs on {BOLD}port 5000{RESET} on your machine.
673
- Your phone needs to reach this port over the internet.
674
-
675
- You have two options:
676
-
677
- {BOLD}Option A — localtunnel (easiest + free){RESET}
678
- localtunnel creates a public HTTPS URL that tunnels to your local port 5000.
679
- No account required.
680
- Official site: {CYAN}https://theboroer.github.io/localtunnel-www/{RESET}
681
-
682
- {BOLD}Option B — Your own port forwarding (advanced){RESET}
683
- Forward {BOLD}TCP port 5000{RESET} on your router to your machine's local IP.
684
- Then use {BOLD}http://<your-public-ip>:5000{RESET} in the mobile app.
685
- Steps:
686
- 1. Find your machine's local IP → ip addr (Linux) / ipconfig (Windows)
687
- 2. Log into your router admin panel (usually http://192.168.1.1)
688
- 3. Add a port forwarding rule: External 5000 → Internal <your-local-IP>:5000
689
- 4. Use your public IP (check https://ipify.org) in the mobile app.
690
- {YELLOW}Note: dynamic public IPs change on router restart — consider a DDNS service.{RESET}
691
-
692
- {DIM}You can also run only on your local WiFi — both phone and PC must be on
693
- the same network. Use your PC's local IP (e.g. 192.168.x.x) in the app.{RESET}
674
+ The SuperBrain backend runs locally. Your phone needs to reach it over the internet.
675
+ We recommend {BOLD}ngrok{RESET} for a secure tunnel.
676
+
677
+ Requires a free account from: {CYAN}https://dashboard.ngrok.com/signup{RESET}
678
+ Get your Authtoken at: {CYAN}https://dashboard.ngrok.com/get-started/your-authtoken{RESET}
694
679
  """)
695
680
 
696
- choice = ask_yn("Enable localtunnel on startup?", default=True)
681
+ choice = ask_yn("Enable ngrok on startup?", default=True)
697
682
  if not choice:
698
- LOCALTUNNEL_ENABLED.unlink(missing_ok=True)
699
- warn("Skipping localtunnel. Use either your own port forwarding or local WiFi.")
700
- info("Remember: set the correct server URL in the mobile app Settings.")
701
- return
702
-
703
- if not shutil.which("npx"):
704
- print(f"""
705
- {YELLOW}npx is not installed / not on PATH.{RESET}
706
-
707
- Install it:
708
- Linux → {CYAN}Install Node.js (includes npm + npx){RESET}
709
- macOS → {CYAN}brew install node{RESET}
710
- Windows → Install Node.js LTS from {CYAN}https://nodejs.org/{RESET}
711
-
712
- After installing, re-run {BOLD}python start.py{RESET}.
713
- """)
714
- warn("Skipping localtunnel setup.")
683
+ NGROK_ENABLED.unlink(missing_ok=True)
684
+ warn("Skipping ngrok. Local WiFi only.")
715
685
  return
716
686
 
717
- ok("npx binary found")
718
- LOCALTUNNEL_ENABLED.parent.mkdir(parents=True, exist_ok=True)
719
- LOCALTUNNEL_ENABLED.write_text("enabled")
720
- ok("localtunnel auto-start enabled")
721
- nl()
722
- info("localtunnel will be started automatically every time you run start.py.")
687
+ ok("ngrok auto-start enabled")
688
+ NGROK_ENABLED.parent.mkdir(parents=True, exist_ok=True)
689
+ NGROK_ENABLED.write_text("enabled")
690
+
691
+ existing_token = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else ""
692
+ print(f"\n {YELLOW}Please paste your ngrok Authtoken.{RESET}")
693
+ if existing_token:
694
+ print(f" {DIM}(Leave blank to keep existing token){RESET}")
695
+
696
+ auth_token = ask("Authtoken", default=existing_token, paste=True)
697
+ if auth_token.strip():
698
+ NGROK_TOKEN.write_text(auth_token.strip())
699
+ ok("ngrok token saved.")
700
+ else:
701
+ warn("No ngrok token provided. ngrok may disconnect. To fix, re-run setup.")
723
702
 
724
703
  # ══════════════════════════════════════════════════════════════════════════════
725
704
  # Step 6 — Access Token & Database
@@ -754,87 +733,21 @@ def setup_token_and_db():
754
733
  # ══════════════════════════════════════════════════════════════════════════════
755
734
  # Launch Backend
756
735
  # ══════════════════════════════════════════════════════════════════════════════
757
- def _extract_localtunnel_url(text: str) -> str | None:
758
- """Extract first localtunnel public URL from text."""
759
- import re
760
- m = re.search(r"https://[\w.-]+\.loca\.lt\b", text)
761
- return m.group(0) if m else None
762
-
763
-
764
- def _find_localtunnel_url_from_log() -> str | None:
765
- """Read local tunnel log and return detected public URL if available."""
766
- try:
767
- if not LOCALTUNNEL_LOG.exists():
768
- return None
769
- text = LOCALTUNNEL_LOG.read_text(encoding="utf-8", errors="ignore")
770
- return _extract_localtunnel_url(text)
771
- except Exception:
772
- return None
773
-
774
-
775
- def _stop_localtunnel_processes():
776
- """Stop existing localtunnel processes so only one tunnel remains active."""
777
- try:
778
- if IS_WINDOWS:
779
- script = (
780
- "Get-CimInstance Win32_Process "
781
- "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
782
- "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
783
- )
784
- subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
785
- else:
786
- subprocess.run(["pkill", "-f", "localtunnel"], check=False)
787
- except Exception:
788
- pass
789
-
790
-
791
- def _start_localtunnel(port: int, timeout: int = 25) -> str | None:
792
- """Start localtunnel in the background and wait for the public URL."""
793
- import time as _time
794
-
795
- npx_exec = shutil.which("npx") or shutil.which("npx.cmd")
796
- if not npx_exec:
797
- return None
798
-
799
- # Clean stale localtunnel processes.
800
- _stop_localtunnel_processes()
801
- _time.sleep(0.8)
802
-
803
- info("Starting localtunnel in background …")
736
+ def _start_ngrok(port: int) -> str | None:
804
737
  try:
805
- LOCALTUNNEL_LOG.parent.mkdir(parents=True, exist_ok=True)
806
- LOCALTUNNEL_LOG.write_text("")
807
-
808
- log_handle = open(LOCALTUNNEL_LOG, "a", encoding="utf-8", buffering=1)
809
- kwargs = {
810
- "start_new_session": True,
811
- "stdout": log_handle,
812
- "stderr": subprocess.STDOUT,
813
- "text": True,
814
- }
815
- if IS_WINDOWS and npx_exec.lower().endswith(".cmd"):
816
- cmd = ["cmd", "/c", npx_exec, "-y", "localtunnel", "--port", str(port)]
817
- else:
818
- cmd = [npx_exec, "-y", "localtunnel", "--port", str(port)]
819
- subprocess.Popen(cmd, **kwargs)
738
+ import pyngrok
739
+ from pyngrok import ngrok
740
+
741
+ token = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else None
742
+ if token:
743
+ ngrok.set_auth_token(token)
744
+
745
+ tunnel = ngrok.connect(port, bind_tls=True)
746
+ return tunnel.public_url
820
747
  except Exception as e:
821
- warn(f"Could not start localtunnel: {e}")
748
+ warn(f"Failed to start ngrok: {e}")
822
749
  return None
823
750
 
824
- # Poll log output until URL is emitted.
825
- deadline = _time.time() + timeout
826
- while _time.time() < deadline:
827
- _time.sleep(1)
828
- url = _find_localtunnel_url_from_log()
829
- if url:
830
- ok(f"localtunnel active → {GREEN}{BOLD}{url}{RESET}")
831
- return url
832
-
833
- warn("localtunnel started but URL is not available yet.")
834
- info(f"Check tunnel logs in: {LOCALTUNNEL_LOG}")
835
- return None
836
-
837
-
838
751
  def _get_windows_pids_on_port(port: int) -> list[int]:
839
752
  """Return listener PIDs on Windows using Get-NetTCPConnection when available."""
840
753
  pids: set[int] = set()
@@ -1202,26 +1115,32 @@ def launch_backend():
1202
1115
  token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "—"
1203
1116
  local_ip = _detect_local_ip()
1204
1117
 
1205
- localtunnel_enabled = bool(shutil.which("npx") or shutil.which("npx.cmd"))
1118
+ # ngrok startup
1119
+ public_url: str | None = None
1120
+ if NGROK_ENABLED.exists():
1121
+ token_txt = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else ""
1122
+ if not token_txt:
1123
+ warn("Ngrok is enabled but no Authtoken was found.")
1124
+ setup_remote_access()
1206
1125
 
1207
- localtunnel_url: str | None = None
1208
- if localtunnel_enabled:
1209
- localtunnel_url = _start_localtunnel(PORT)
1210
- else:
1211
- localtunnel_url = _find_localtunnel_url_from_log()
1212
-
1213
- if localtunnel_url:
1214
- tunnel_line = f" Public URL → {GREEN}{BOLD}{localtunnel_url}{RESET} {DIM}(localtunnel){RESET}"
1215
- tunnel_hint = f" · public → {GREEN}{localtunnel_url}{RESET}"
1216
- elif localtunnel_enabled:
1217
- tunnel_line = f" Public URL → {YELLOW}(starting — URL pending, check localtunnel.log){RESET}"
1218
- tunnel_hint = f" · public run: {DIM}npx localtunnel --port {PORT}{RESET}"
1126
+ info("Starting ngrok in background...")
1127
+ public_url = _start_ngrok(PORT)
1128
+
1129
+ tunnel_line = ""
1130
+ tunnel_hint = ""
1131
+
1132
+ if public_url:
1133
+ tunnel_line = f" Public URL → {GREEN}{BOLD}{public_url}{RESET} {DIM}(ngrok){RESET}"
1134
+ tunnel_hint = f" · public → {GREEN}{public_url}{RESET}"
1135
+ ok(f"ngrok active → {GREEN}{BOLD}{public_url}{RESET}")
1136
+ elif NGROK_ENABLED.exists():
1137
+ tunnel_line = f" Public URL → {YELLOW}(failed to start ngrok){RESET}"
1138
+ tunnel_hint = f" · public → run manually: {DIM}ngrok http {PORT}{RESET}"
1219
1139
  else:
1220
- tunnel_line = ""
1221
- tunnel_hint = f" · public → install Node.js first, then run: {DIM}npx localtunnel --port {PORT}{RESET}"
1140
+ tunnel_hint = f" · public → enable ngrok via {DIM}python start.py --reset{RESET}"
1222
1141
 
1223
1142
  # ── Generate and display QR code ──────────────────────────────────────────
1224
- qr_url = localtunnel_url if localtunnel_url else f"http://{local_ip}:{PORT}"
1143
+ qr_url = public_url if public_url else f"http://{local_ip}:{PORT}"
1225
1144
  _display_connect_qr(qr_url, token)
1226
1145
 
1227
1146
  print(f"""
@@ -1274,15 +1193,21 @@ def launch_backend_status():
1274
1193
  if TOKEN_FILE.exists():
1275
1194
  token = TOKEN_FILE.read_text(encoding="utf-8").strip()
1276
1195
 
1196
+ # Fetch ngrok status via API
1277
1197
  url = "NOT_FOUND"
1278
- log_file = BASE_DIR / "config" / "localtunnel.log"
1279
- if log_file.exists():
1280
- match = re.search(r"your url is: (https://[^\s]+)", log_file.read_text(encoding="utf-8"))
1281
- if match:
1282
- url = match.group(1)
1283
-
1198
+ try:
1199
+ import urllib.request, json
1200
+ req = urllib.request.urlopen("http://127.0.0.1:4040/api/tunnels", timeout=2)
1201
+ data = json.loads(req.read())
1202
+ for tunnel in data.get("tunnels", []):
1203
+ if tunnel.get("proto") == "https":
1204
+ url = tunnel.get("public_url")
1205
+ break
1206
+ except Exception:
1207
+ pass
1208
+
1284
1209
  if url == "NOT_FOUND":
1285
- warn("Could not find a running localtunnel URL in config/localtunnel.log.")
1210
+ warn("Could not find a running ngrok URL. Is the server running?")
1286
1211
  nl()
1287
1212
  print(" Wait 5 seconds, or run 'superbrain-server' to start the server.")
1288
1213
  return
@@ -1310,6 +1235,14 @@ def main():
1310
1235
  launch_backend_status()
1311
1236
  return
1312
1237
 
1238
+ ngrok_mode = "--ngrok" in sys.argv
1239
+ if ngrok_mode:
1240
+ h1("SuperBrain Ngrok Configuration")
1241
+ setup_remote_access()
1242
+ nl()
1243
+ ok("Ngrok configuration finished. Run 'superbrain-server' to start the backend.")
1244
+ return
1245
+
1313
1246
  reset_mode = "--reset" in sys.argv
1314
1247
 
1315
1248
  if SETUP_DONE.exists() and not reset_mode:
@@ -1 +0,0 @@
1
- 3f491f84-184a-40ba-9660-b882b881c4d5
@@ -1,86 +0,0 @@
1
-
2
- ===============================================================================
3
- Welcome to localhost.run!
4
-
5
- Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
6
-
7
- To set up and manage custom domains go to https://admin.localhost.run/
8
-
9
- More details on custom domains (and how to enable subdomains of your custom
10
- domain) at https://localhost.run/docs/custom-domains
11
-
12
- If you get a permission denied error check the faq for how to connect with a key or
13
- create a free tunnel without a key at [http://localhost:3000/docs/faq#generating-an-ssh-key].
14
-
15
- To explore using localhost.run visit the documentation site:
16
- https://localhost.run/docs/
17
-
18
- ===============================================================================
19
-
20
- ** your connection id is 78ac0b8b-a8f4-4185-9da5-a446d15e032f, please mention it if you send me a message about an issue. **
21
-
22
- authenticated as anonymous user
23
- 14ebb2ac2ccab3.lhr.life tunneled with tls termination, https://14ebb2ac2ccab3.lhr.life
24
- create an account and add your key for a longer lasting domain name. see https://localhost.run/docs/forever-free/ for more information.
25
- Open your tunnel address on your mobile with this QR:
26
-
27
-                            
28
-                            
29
-                            
30
-                            
31
-                            
32
-                            
33
-                            
34
-                            
35
-                            
36
-                            
37
-                            
38
-                            
39
-                            
40
-                            
41
-                            
42
-                            
43
-                            
44
-                            
45
-                            
46
-                            
47
-                            
48
-                            
49
-                            
50
-                            
51
-                            
52
-                            
53
-                            
54
-
55
- c3f3097e2d129a.lhr.life tunneled with tls termination, https://c3f3097e2d129a.lhr.life
56
- create an account and add your key for a longer lasting domain name. see https://localhost.run/docs/forever-free/ for more information.
57
- Open your tunnel address on your mobile with this QR:
58
-
59
-                            
60
-                            
61
-                            
62
-                            
63
-                            
64
-                            
65
-                            
66
-                            
67
-                            
68
-                            
69
-                            
70
-                            
71
-                            
72
-                            
73
-                            
74
-                            
75
-                            
76
-                            
77
-                            
78
-                            
79
-                            
80
-                            
81
-                            
82
-                            
83
-                            
84
-                            
85
-                            
86
-
@@ -1,241 +0,0 @@
1
- """
2
- SuperBrain - Comprehensive Backend Test Suite
3
- Tests all API endpoints against the running backend.
4
- """
5
- import requests
6
- import sys
7
- import json
8
- import time
9
-
10
- # Config
11
- BASE_URL = "http://localhost:5000"
12
- TOKEN = "4XVTLWWV"
13
- HEADERS = {"X-API-Key": TOKEN, "Content-Type": "application/json"}
14
-
15
- TEST_YT_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
16
-
17
- passed = 0
18
- failed = 0
19
- skipped = 0
20
-
21
- def test(name, condition, detail=""):
22
- global passed, failed
23
- if condition:
24
- passed += 1
25
- print(f" [PASS] {name}")
26
- else:
27
- failed += 1
28
- print(f" [FAIL] {name} -- {detail}")
29
-
30
- def skip(name, reason):
31
- global skipped
32
- skipped += 1
33
- print(f" [SKIP] {name} -- {reason}")
34
-
35
- # Phase 1: Health & Auth
36
- print("\n--- Phase 1: Health & Auth ---")
37
-
38
- try:
39
- r = requests.get(f"{BASE_URL}/status", timeout=5)
40
- test("1.1 GET /status", r.status_code == 200 and r.json().get("status") == "online", f"status={r.status_code}")
41
- except Exception as e:
42
- test("1.1 GET /status", False, str(e))
43
-
44
- try:
45
- r = requests.get(f"{BASE_URL}/health", headers=HEADERS, timeout=5)
46
- data = r.json()
47
- test("1.2 GET /health (auth)", r.status_code == 200 and "database" in data, f"status={r.status_code}")
48
- except Exception as e:
49
- test("1.2 GET /health", False, str(e))
50
-
51
- try:
52
- r = requests.get(f"{BASE_URL}/health", headers={"X-API-Key": "WRONGTKN"}, timeout=5)
53
- test("1.3 Auth rejection (wrong)", r.status_code == 401, f"status={r.status_code}")
54
- except Exception as e:
55
- test("1.3 Auth rejection", False, str(e))
56
-
57
- try:
58
- r = requests.get(f"{BASE_URL}/health", timeout=5)
59
- test("1.4 Auth rejection (none)", r.status_code in [401, 403, 422], f"status={r.status_code}")
60
- except Exception as e:
61
- test("1.4 Auth rejection (none)", False, str(e))
62
-
63
- # Phase 2: Read Endpoints
64
- print("\n--- Phase 2: Read Endpoints ---")
65
-
66
- try:
67
- r = requests.get(f"{BASE_URL}/recent?limit=10", headers=HEADERS, timeout=10)
68
- data = r.json()
69
- test("2.1 GET /recent", r.status_code == 200 and "data" in data, f"status={r.status_code}")
70
- print(f" found {len(data.get('data', []))} posts")
71
- except Exception as e:
72
- test("2.1 GET /recent", False, str(e))
73
-
74
- try:
75
- r = requests.get(f"{BASE_URL}/categories", headers=HEADERS, timeout=10)
76
- data = r.json()
77
- test("2.2 GET /categories", r.status_code == 200 and "categories" in data, f"status={r.status_code}")
78
- print(f" found {len(data.get('categories', []))} categories")
79
- except Exception as e:
80
- test("2.2 GET /categories", False, str(e))
81
-
82
- try:
83
- r = requests.get(f"{BASE_URL}/queue-status", headers=HEADERS, timeout=10)
84
- data = r.json()
85
- test("2.3 GET /queue-status", r.status_code == 200 and "processing_count" in data, f"keys={list(data.keys())}")
86
- print(f" processing={data.get('processing_count',0)}, queue={data.get('queue_count',0)}, retry={data.get('retry_count',0)}")
87
- except Exception as e:
88
- test("2.3 GET /queue-status", False, str(e))
89
-
90
- try:
91
- r = requests.get(f"{BASE_URL}/stats", headers=HEADERS, timeout=10)
92
- test("2.4 GET /stats", r.status_code == 200, f"status={r.status_code}")
93
- except Exception as e:
94
- test("2.4 GET /stats", False, str(e))
95
-
96
- try:
97
- r = requests.get(f"{BASE_URL}/search?tags=travel&limit=5", headers=HEADERS, timeout=10)
98
- test("2.5 GET /search?tags=travel", r.status_code == 200, f"status={r.status_code}")
99
- except Exception as e:
100
- test("2.5 GET /search", False, str(e))
101
-
102
- try:
103
- r = requests.get(f"{BASE_URL}/queue/retry", headers=HEADERS, timeout=10)
104
- test("2.6 GET /queue/retry", r.status_code == 200, f"status={r.status_code}")
105
- except Exception as e:
106
- test("2.6 GET /queue/retry", False, str(e))
107
-
108
- # Phase 3: Collections CRUD
109
- print("\n--- Phase 3: Collections CRUD ---")
110
-
111
- test_collection_id = None
112
-
113
- try:
114
- r = requests.get(f"{BASE_URL}/collections", headers=HEADERS, timeout=10)
115
- data = r.json()
116
- test("3.1 GET /collections", r.status_code == 200, f"status={r.status_code}")
117
- print(f" found {len(data)} collections")
118
- except Exception as e:
119
- test("3.1 GET /collections", False, str(e))
120
-
121
- try:
122
- r = requests.post(f"{BASE_URL}/collections", headers=HEADERS, json={"id": "test-id", "name": "Test Collection", "icon": "test"}, timeout=10)
123
- data = r.json()
124
- test("3.2 POST /collections (create)", r.status_code == 200 and data.get("id"), f"status={r.status_code}")
125
- test_collection_id = data.get("id")
126
- print(f" created ID: {test_collection_id}")
127
- except Exception as e:
128
- test("3.2 POST /collections", False, str(e))
129
-
130
- if test_collection_id:
131
- try:
132
- r = requests.post(f"{BASE_URL}/collections", headers=HEADERS, json={"id": test_collection_id, "name": "Updated Test", "icon": "ok"}, timeout=10)
133
- test("3.3 POST /collections (update)", r.status_code == 200, f"status={r.status_code}")
134
- except Exception as e:
135
- test("3.3 POST /collections (update)", False, str(e))
136
-
137
- try:
138
- r = requests.delete(f"{BASE_URL}/collections/{test_collection_id}", headers=HEADERS, timeout=10)
139
- test("3.4 DELETE /collections", r.status_code == 200, f"status={r.status_code}")
140
- except Exception as e:
141
- test("3.4 DELETE /collections", False, str(e))
142
- else:
143
- skip("3.3 PUT /collections", "no collection created")
144
- skip("3.4 DELETE /collections", "no collection created")
145
-
146
- try:
147
- r = requests.get(f"{BASE_URL}/collections", headers=HEADERS, timeout=10)
148
- data = r.json()
149
- collections = data.get("data", [])
150
- wl = next((c for c in collections if c.get("name") == "Watch Later"), None)
151
- if wl:
152
- r = requests.delete(f"{BASE_URL}/collections/{wl['id']}", headers=HEADERS, timeout=10)
153
- test("3.5 Watch Later protection", r.status_code in [400, 403], f"status={r.status_code} (should be blocked)")
154
- else:
155
- skip("3.5 Watch Later protection", "no Watch Later found")
156
- except Exception as e:
157
- test("3.5 Watch Later protection", False, str(e))
158
-
159
- # Phase 4: Settings
160
- print("\n--- Phase 4: Settings ---")
161
-
162
- try:
163
- r = requests.get(f"{BASE_URL}/settings/ai-providers", headers=HEADERS, timeout=10)
164
- test("4.1 GET /settings/ai-providers", r.status_code == 200, f"status={r.status_code}")
165
- except Exception as e:
166
- test("4.1 GET /settings/ai-providers", False, str(e))
167
-
168
- try:
169
- r = requests.post(f"{BASE_URL}/settings/ai-providers", headers=HEADERS,
170
- json={"provider": "groq", "api_key": "gsk_test"}, timeout=10)
171
- test("4.2 POST ai-providers (Groq)", r.status_code == 200, f"status={r.status_code}")
172
- except Exception as e:
173
- test("4.2 POST ai-providers", False, str(e))
174
-
175
- try:
176
- r = requests.get(f"{BASE_URL}/settings/instagram", headers=HEADERS, timeout=10)
177
- test("4.3 GET /settings/instagram", r.status_code == 200, f"status={r.status_code}")
178
- except Exception as e:
179
- test("4.3 GET /settings/instagram", False, str(e))
180
-
181
- try:
182
- r = requests.post(f"{BASE_URL}/settings/instagram", headers=HEADERS,
183
- json={"username": "testuser", "password": "testpass"}, timeout=10)
184
- test("4.4 POST /settings/instagram", r.status_code == 200, f"status={r.status_code}")
185
- except Exception as e:
186
- test("4.4 POST /settings/instagram", False, str(e))
187
-
188
- # Phase 5: Export/Import
189
- print("\n--- Phase 5: Export/Import ---")
190
-
191
- try:
192
- r = requests.get(f"{BASE_URL}/export", headers=HEADERS, timeout=15)
193
- test("5.1 GET /export", r.status_code == 200, f"status={r.status_code}")
194
- ed = r.json()
195
- print(f" exported {len(ed.get('posts', []))} posts, {len(ed.get('collections', []))} collections")
196
- except Exception as e:
197
- test("5.1 GET /export", False, str(e))
198
-
199
- try:
200
- r = requests.post(f"{BASE_URL}/import", headers=HEADERS,
201
- json={"posts": [], "collections": [], "mode": "merge"}, timeout=15)
202
- test("5.2 POST /import (merge, empty)", r.status_code == 200, f"status={r.status_code}")
203
- except Exception as e:
204
- test("5.2 POST /import", False, str(e))
205
-
206
- # Phase 6: Analysis
207
- print("\n--- Phase 6: Analysis (YouTube) ---")
208
-
209
- try:
210
- print(" submitting YouTube URL... (timeout 90s)")
211
- r = requests.post(f"{BASE_URL}/analyze", headers=HEADERS,
212
- json={"url": TEST_YT_URL}, timeout=90)
213
- data = r.json()
214
- if r.status_code == 200:
215
- test("6.1 POST /analyze (YouTube)", data.get("success") == True, f"success={data.get('success')}")
216
- if data.get("data"):
217
- d = data["data"]
218
- print(f" title: {d.get('title', 'N/A')[:60]}")
219
- print(f" category: {d.get('category', 'N/A')}")
220
- print(f" tags: {str(d.get('tags', [])[:5])}")
221
- print(f" cached: {data.get('cached', False)}")
222
- elif r.status_code == 202:
223
- skip("6.1 POST /analyze (YouTube)", "queued for retry (quota)")
224
- elif r.status_code == 503:
225
- skip("6.1 POST /analyze (YouTube)", "503 server busy")
226
- else:
227
- test("6.1 POST /analyze (YouTube)", False, f"status={r.status_code}")
228
- except requests.exceptions.Timeout:
229
- skip("6.1 POST /analyze (YouTube)", "timed out (90s)")
230
- except Exception as e:
231
- test("6.1 POST /analyze (YouTube)", False, str(e))
232
-
233
- # Summary
234
- print("\n" + "=" * 50)
235
- print(f" PASSED: {passed}")
236
- print(f" FAILED: {failed}")
237
- print(f" SKIPPED: {skipped}")
238
- print(f" TOTAL: {passed + failed + skipped}")
239
- print("=" * 50)
240
-
241
- sys.exit(1 if failed > 0 else 0)
File without changes
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env python3
2
- import requests
3
- import json
4
-
5
- # Read token
6
- with open('token.txt', 'r') as f:
7
- token = f.read().strip()
8
-
9
- # Test the /recent endpoint
10
- response = requests.get(
11
- 'http://localhost:5000/recent?limit=10',
12
- headers={'X-API-Key': token}
13
- )
14
-
15
- print(f"Status Code: {response.status_code}")
16
- print(f"\nResponse:")
17
- print(json.dumps(response.json(), indent=2))
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env python3
2
- import sys
3
- from pathlib import Path
4
-
5
- # Ensure backend root is in sys.path
6
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
7
-
8
- from core.database import get_db
9
-
10
- db = get_db()
11
- posts = db.get_recent(5)
12
- print(f'Found {len(posts)} posts in database')
13
- print()
14
-
15
- for p in posts:
16
- print(f"Shortcode: {p.get('shortcode', 'N/A')}")
17
- print(f"Title: {p.get('title', 'N/A')}")
18
- print(f"Username: {p.get('username', 'N/A')}")
19
- print(f"URL: {p.get('url', 'N/A')}")
20
- print(f"Category: {p.get('category', 'N/A')}")
21
- print(f"Analyzed: {p.get('analyzed_at', 'N/A')}")
22
- print("-" * 60)
@@ -1,65 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Legacy filename retained for compatibility.
4
- This script now validates API key authentication behavior.
5
- """
6
-
7
- import sys
8
- from pathlib import Path
9
-
10
- from fastapi.testclient import TestClient
11
-
12
- # Ensure backend root is in sys.path
13
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
14
-
15
- from api import app, API_TOKEN, generate_api_token # noqa: E402
16
-
17
-
18
- client = TestClient(app)
19
-
20
-
21
- def test_generate_api_token_shape():
22
- token = generate_api_token()
23
- assert isinstance(token, str)
24
- assert len(token) == 8
25
- assert token.isalnum()
26
-
27
-
28
- def test_ping_works_without_auth():
29
- response = client.get('/ping')
30
- assert response.status_code == 200
31
-
32
-
33
- def test_queue_status_rejects_invalid_api_key():
34
- response = client.get('/queue-status', headers={'X-API-Key': 'INVALID_TOKEN'})
35
- assert response.status_code == 401
36
-
37
-
38
- def test_queue_status_accepts_valid_api_key():
39
- response = client.get('/queue-status', headers={'X-API-Key': API_TOKEN})
40
- assert response.status_code == 200
41
-
42
-
43
- def test_connect_endpoint_is_deprecated():
44
- response = client.post('/connect', json={'api_key': API_TOKEN})
45
- assert response.status_code == 410
46
-
47
-
48
- if __name__ == '__main__':
49
- print('Running API key auth tests...')
50
- test_generate_api_token_shape()
51
- print('PASS: API token generation shape')
52
-
53
- test_ping_works_without_auth()
54
- print('PASS: /ping unauthenticated access')
55
-
56
- test_queue_status_rejects_invalid_api_key()
57
- print('PASS: /queue-status rejects invalid API key')
58
-
59
- test_queue_status_accepts_valid_api_key()
60
- print('PASS: /queue-status accepts valid API key')
61
-
62
- test_connect_endpoint_is_deprecated()
63
- print('PASS: /connect deprecated behavior')
64
-
65
- print('\nAll API key auth tests passed!')