aliSaac510 commited on
Commit
4c19c61
·
verified ·
1 Parent(s): 9479a8e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -220
app.py CHANGED
@@ -1,267 +1,138 @@
1
  import os
2
  import json
3
- import re
4
  from typing import List, Dict, Any
5
- from fastapi import FastAPI, HTTPException, BackgroundTasks
6
- from pydantic import BaseModel
7
  from dotenv import load_dotenv
8
  from supabase import create_client, Client
9
  from groq import Groq
10
  from google import genai
11
  import uvicorn
12
- import csv
13
 
14
- # Load environment variables
15
  load_dotenv()
16
 
17
- app = FastAPI(title="Supabase Data Linker API (Structured Edition)")
18
 
19
- # Configuration
20
  SUPABASE_URL = os.getenv("SUPABASE_URL")
21
  SUPABASE_KEY = os.getenv("SUPABASE_ANON_KEY")
22
- GROQ_API_KEYS = os.getenv("GROQ_API_KEYS", "").split(",")
23
- GEMINI_API_KEYS = os.getenv("GEMINI_API_KEYS", "").split(",")
24
 
25
- # Initialize Supabase
26
  if not SUPABASE_URL or not SUPABASE_KEY:
27
- raise ValueError("Missing SUPABASE environment variables")
 
28
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
29
 
30
- class MultiProviderManager:
31
- """Manages rotation for both Groq and Gemini keys."""
32
- def __init__(self, groq_keys: List[str], gemini_keys: List[str]):
33
- self.providers = {
34
- "groq": [k.strip() for k in groq_keys if k.strip()],
35
- "gemini": [k.strip() for k in gemini_keys if k.strip()]
36
- }
37
  self.indices = {"groq": 0, "gemini": 0}
38
- self.current_provider = "groq" if self.providers["groq"] else "gemini"
39
-
40
- def get_session(self):
41
- if not self.providers[self.current_provider]:
42
- if self.current_provider == "groq" and self.providers["gemini"]:
43
- self.current_provider = "gemini"
44
- print("🔀 Groq not available, switching to Gemini...")
45
- return self.get_session()
46
- raise ValueError("No API keys found for the current provider or fallback.")
47
-
48
- key = self.providers[self.current_provider][self.indices[self.current_provider]]
49
 
50
- if self.current_provider == "groq":
51
- return Groq(api_key=key), "groq"
52
- else:
53
- return genai.Client(api_key=key), "gemini"
54
-
55
- def rotate_current(self):
56
- """Try next key in current provider, or switch provider."""
57
- current_keys = self.providers[self.current_provider]
58
- if self.indices[self.current_provider] + 1 < len(current_keys):
59
- self.indices[self.current_provider] += 1
60
- print(f"🔄 Rotating to {self.current_provider.upper()} Key #{self.indices[self.current_provider] + 1}")
 
61
  else:
62
- if self.current_provider == "groq" and self.providers["gemini"]:
63
- self.current_provider = "gemini"
64
- self.indices["gemini"] = 0
65
- print("⚠️ Groq Keys exhausted. Switching to Gemini provider.")
66
- else:
67
- raise Exception(f"All keys for {self.current_provider} exhausted.")
68
-
69
- api_manager = MultiProviderManager(GROQ_API_KEYS, GEMINI_API_KEYS)
70
-
71
- # --- Helper Functions ---
72
 
73
- def fetch_supabase_data(table_name: str):
74
- try:
75
- response = supabase.table(table_name).select("*").execute()
76
- return response.data
77
- except Exception as e:
78
- print(f"Error fetching {table_name}: {e}")
79
- return []
80
 
81
- def load_existing_links_from_db():
82
- try:
83
- response = supabase.table("linked_results").select("short_id").execute()
84
- return response.data
85
- except Exception as e:
86
- print(f"Error loading links from DB: {e}")
87
- return []
88
 
89
- def save_links_to_db(links: List[Dict[str, Any]]):
90
- if not links: return 0
91
- try:
92
- data = [{
93
- "short_id": l["short_id"],
94
- "media_id": l["media_id"],
95
- "short_title": l["short_title"],
96
- "media_title": l["media_title"],
97
- "short_link": l["short_link"],
98
- "media_link": l["media_link"]
99
- } for l in links]
100
- response = supabase.table("linked_results").insert(data).execute()
101
- return len(response.data)
102
- except Exception as e:
103
- print(f"❌ DB Save Error: {e}")
104
- return 0
105
-
106
- # --- Linking Logic ---
107
-
108
- def call_ai(session, provider, prompt):
109
  if provider == "groq":
110
- completion = session.chat.completions.create(
111
  messages=[{"role": "user", "content": prompt}],
112
  model="llama-3.3-70b-versatile",
113
  response_format={"type": "json_object"}
114
  )
115
- return json.loads(completion.choices[0].message.content)
116
- else:
117
- response = session.models.generate_content(
118
- model="gemini-3-flash-preview",
119
- contents=prompt,
120
- config={'response_mime_type': 'application/json'}
121
- )
122
- return json.loads(response.text)
123
-
124
- def perform_smart_linking(shorts, movies, batch_size=20):
125
- all_matches = []
126
 
127
- # Process media for AI
128
- movies_ref = [
129
- {
130
- "id": m["id"],
131
- "title": m["title"],
132
- "type": m.get("type", "unknown"),
133
- "year": str(m.get("releasdate", ""))[:4],
134
- "description": m.get("dec", "")[:150],
135
- "url": m.get("url") or f"https://flixhq.to/{m.get('id_slug','')}"
136
- } for m in movies
137
- ]
138
-
139
- # Process shorts for AI (Optimized for structured data)
140
- shorts_list = []
141
- for s in shorts:
142
- shorts_list.append({
143
- "id": s.get("id"),
144
- "title": s.get("title", s.get("yt_title", "Unknown")),
145
- "description": s.get("description", "")[:100], # Pass some description for context
146
- "url": s.get("url") or s.get("yt_link", "N/A")
147
- })
148
-
149
- for i in range(0, len(shorts_list), batch_size):
150
- batch = shorts_list[i : i + batch_size]
151
- success = False
152
-
153
- while not success:
154
- try:
155
- session, provider = api_manager.get_session()
156
- prompt = f"""
157
- Match these 'shorts' to their corresponding 'movies/tv series' (media).
158
- Reference Media: {json.dumps(movies_ref[:120])}
159
- Target Shorts: {json.dumps(batch)}
160
- Respond ONLY with JSON: {{"matches": [{{"short_id", "media_id", "short_title", "media_title", "short_link", "media_link"}}]}}
161
- """
162
-
163
- result = call_ai(session, provider, prompt)
164
- if "matches" in result:
165
- all_matches.extend(result["matches"])
166
- success = True
167
- except Exception as e:
168
- err = str(e).lower()
169
- if any(x in err for x in ["rate_limit", "429", "413"]):
170
- try:
171
- api_manager.rotate_current()
172
- except:
173
- print("🛑 All Keys/Providers exhausted for this batch.")
174
- break
175
- else:
176
- print(f"❌ Error in batch {i}: {e}")
177
- break
178
- return all_matches
179
 
180
  # --- Endpoints ---
181
 
182
  @app.get("/")
183
  def home():
184
- return {"status": "online", "message": "Supabase Linked API (Structured Shorts Edition)"}
185
 
186
  @app.get("/status")
187
  def status():
188
- existing = load_existing_links_from_db()
189
  return {
190
- "db_linked_count": len(existing),
191
- "groq_keys": len(api_manager.providers["groq"]),
192
- "gemini_keys": len(api_manager.providers["gemini"]),
193
- "current_provider": api_manager.current_provider
194
  }
195
 
196
- @app.get("/test-db")
197
- def test_db():
198
- try:
199
- test_data = {
200
- "short_id": 999999,
201
- "media_id": 1,
202
- "short_title": "Test DB Access",
203
- "media_title": "Test Movie",
204
- "short_link": "http://test.com",
205
- "media_link": "http://test.com"
206
- }
207
- supabase.table("linked_results").insert(test_data).execute()
208
- supabase.table("linked_results").delete().eq("short_id", 999999).execute()
209
- return {"status": "success", "message": "Database is working!"}
210
- except Exception as e:
211
- return {"status": "failed", "error": str(e)}
212
-
213
- @app.get("/test-integration")
214
- def test_integration():
215
- if not api_manager.providers["gemini"]:
216
- return {"error": "No Gemini Keys configured"}
217
- try:
218
- client = genai.Client(api_key=api_manager.providers["gemini"][0])
219
- prompt = """Generate one realistic mock linked result JSON."""
220
- response = client.models.generate_content(
221
- model="gemini-3-flash-preview",
222
- contents=prompt,
223
- config={'response_mime_type': 'application/json'}
224
- )
225
- mock_data = json.loads(response.text)
226
- # Handle cases where AI returns a list or direct object
227
- if isinstance(mock_data, list): mock_data = mock_data[0]
228
-
229
- # Ensure keys match DB
230
- test_entry = {
231
- "short_id": mock_data.get("short_id", 888888),
232
- "media_id": mock_data.get("media_id", 12345),
233
- "short_title": mock_data.get("short_title", "Mock Short"),
234
- "media_title": mock_data.get("media_title", "Mock Media"),
235
- "short_link": mock_data.get("short_link", "http://yt.com"),
236
- "media_link": mock_data.get("media_link", "http://flix.com")
237
- }
238
- res = supabase.table("linked_results").insert(test_entry).execute()
239
- return {"status": "success", "data_generated": test_entry}
240
- except Exception as e:
241
- return {"status": "failed", "error": str(e)}
242
-
243
  @app.post("/link-data")
244
- def trigger():
245
- shorts = fetch_supabase_data("shorts")
246
- media = fetch_supabase_data("media")
247
-
248
- if not shorts or not media:
249
- raise HTTPException(status_code=500, detail="Data missing")
250
-
251
- existing_ids = {str(l["short_id"]) for l in load_existing_links_from_db()}
252
- new_shorts = [s for s in shorts if str(s.get("id")) not in existing_ids]
253
 
254
- if not new_shorts:
255
- return {"message": "All caught up!"}
256
-
257
- print(f"🚀 Processing {len(new_shorts)} new structured shorts...")
258
- matches = perform_smart_linking(new_shorts, media)
259
- saved = save_links_to_db(matches)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- return {
262
- "newly_linked": saved,
263
- "total_in_db": len(load_existing_links_from_db())
264
- }
265
 
266
  if __name__ == "__main__":
267
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  import os
2
  import json
 
3
  from typing import List, Dict, Any
4
+ from fastapi import FastAPI, HTTPException
 
5
  from dotenv import load_dotenv
6
  from supabase import create_client, Client
7
  from groq import Groq
8
  from google import genai
9
  import uvicorn
 
10
 
 
11
  load_dotenv()
12
 
13
+ app = FastAPI(title="Movie Linker API (Production)")
14
 
15
+ # --- Configuration ---
16
  SUPABASE_URL = os.getenv("SUPABASE_URL")
17
  SUPABASE_KEY = os.getenv("SUPABASE_ANON_KEY")
18
+ GROQ_API_KEYS = [k.strip() for k in os.getenv("GROQ_API_KEYS", "").split(",") if k.strip()]
19
+ GEMINI_API_KEYS = [k.strip() for k in os.getenv("GEMINI_API_KEYS", "").split(",") if k.strip()]
20
 
 
21
  if not SUPABASE_URL or not SUPABASE_KEY:
22
+ raise ValueError("Missing Supabase credentials in environment variables.")
23
+
24
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
25
 
26
+ class APIKeyManager:
27
+ """Manages rotation between Groq and Gemini providers and their keys."""
28
+ def __init__(self, groq_keys, gemini_keys):
29
+ self.keys = {"groq": groq_keys, "gemini": gemini_keys}
 
 
 
30
  self.indices = {"groq": 0, "gemini": 0}
31
+ self.provider = "groq" if groq_keys else "gemini"
32
+
33
+ def get_client(self):
34
+ if not self.keys[self.provider]:
35
+ if self.provider == "groq" and self.keys["gemini"]:
36
+ self.provider = "gemini"
37
+ return self.get_client()
38
+ raise Exception("No API keys available.")
 
 
 
39
 
40
+ current_key = self.keys[self.provider][self.indices[self.provider]]
41
+ if self.provider == "groq":
42
+ return Groq(api_key=current_key), "groq"
43
+ return genai.Client(api_key=current_key), "gemini"
44
+
45
+ def rotate(self):
46
+ if self.indices[self.provider] + 1 < len(self.keys[self.provider]):
47
+ self.indices[self.provider] += 1
48
+ print(f"🔄 Switched to {self.provider} key #{self.indices[self.provider]+1}")
49
+ elif self.provider == "groq" and self.keys["gemini"]:
50
+ self.provider = "gemini"
51
+ print("⚠️ Groq exhausted, falling back to Gemini.")
52
  else:
53
+ raise Exception("All API keys for all providers exhausted.")
 
 
 
 
 
 
 
 
 
54
 
55
+ key_manager = APIKeyManager(GROQ_API_KEYS, GEMINI_API_KEYS)
 
 
 
 
 
 
56
 
57
+ # --- Core Functions ---
 
 
 
 
 
 
58
 
59
+ def ai_match(client, provider, prompt):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  if provider == "groq":
61
+ res = client.chat.completions.create(
62
  messages=[{"role": "user", "content": prompt}],
63
  model="llama-3.3-70b-versatile",
64
  response_format={"type": "json_object"}
65
  )
66
+ return json.loads(res.choices[0].message.content)
 
 
 
 
 
 
 
 
 
 
67
 
68
+ res = client.models.generate_content(
69
+ model="gemini-3-flash-preview",
70
+ contents=prompt,
71
+ config={'response_mime_type': 'application/json'}
72
+ )
73
+ return json.loads(res.text)
74
+
75
+ def get_db_data(table: str, select: str = "*"):
76
+ return supabase.table(table).select(select).execute().data
77
+
78
+ def process_batch(shorts_batch, movies_ref):
79
+ while True:
80
+ try:
81
+ client, provider = key_manager.get_client()
82
+ prompt = f"Match these shorts to media. Media: {json.dumps(movies_ref[:120])}. Shorts: {json.dumps(shorts_batch)}. Respond with JSON: {{'matches': [{{'short_id', 'media_id', 'short_title', 'media_title', 'short_link', 'media_link'}}]}}"
83
+ return ai_match(client, provider, prompt).get("matches", [])
84
+ except Exception as e:
85
+ if any(x in str(e).lower() for x in ["rate_limit", "429", "limit_reached"]):
86
+ key_manager.rotate()
87
+ else:
88
+ print(f"❌ Batch Error: {e}")
89
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  # --- Endpoints ---
92
 
93
  @app.get("/")
94
  def home():
95
+ return {"status": "online", "engine": "Groq+Gemini"}
96
 
97
  @app.get("/status")
98
  def status():
99
+ linked = get_db_data("linked_results", "short_id")
100
  return {
101
+ "linked_count": len(linked),
102
+ "provider": key_manager.provider,
103
+ "active_keys": {p: len(k) for p, k in key_manager.keys.items()}
 
104
  }
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  @app.post("/link-data")
107
+ def link_data():
108
+ shorts = get_db_data("shorts")
109
+ media = get_db_data("media")
110
+ linked_ids = {str(l["short_id"]) for l in get_db_data("linked_results", "short_id")}
 
 
 
 
 
111
 
112
+ # Filter only new shorts
113
+ new_shorts = [s for s in shorts if str(s.get("id")) not in linked_ids]
114
+ if not new_shorts: return {"message": "Everything is already linked."}
115
+
116
+ # Prepare data for AI
117
+ movies_ref = [{
118
+ "id": m["id"], "title": m["title"], "type": m.get("type"),
119
+ "year": str(m.get("releasdate", ""))[:4], "desc": m.get("dec", "")[:100]
120
+ } for m in media]
121
+
122
+ batch_shorts = [{
123
+ "id": s["id"], "title": s.get("title"), "desc": s.get("description", "")[:100]
124
+ } for s in new_shorts]
125
+
126
+ # Matching Loop
127
+ all_matches = []
128
+ for i in range(0, len(batch_shorts), 20):
129
+ all_matches.extend(process_batch(batch_shorts[i : i + 20], movies_ref))
130
+
131
+ # Save Results
132
+ if all_matches:
133
+ supabase.table("linked_results").insert(all_matches).execute()
134
 
135
+ return {"newly_linked": len(all_matches), "total": len(linked_ids) + len(all_matches)}
 
 
 
136
 
137
  if __name__ == "__main__":
138
  uvicorn.run(app, host="0.0.0.0", port=7860)