aliSaac510 commited on
Commit
591fa53
·
verified ·
1 Parent(s): 9028687

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -101
app.py CHANGED
@@ -1,122 +1,267 @@
1
  import os
 
2
  import re
3
- from fastapi import FastAPI, Body, HTTPException, Query
4
- from googleapiclient.discovery import build
 
5
  from dotenv import load_dotenv
6
- from typing import List, Dict, Any, Optional
 
 
 
 
7
 
8
- # Load environment variables from .env file
9
  load_dotenv()
10
 
11
- app = FastAPI()
12
 
13
- # YouTube Data API Key
14
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
 
 
 
15
 
16
- def find_relevant_youtube_shorts(movie_title: str, order: str = "relevance", max_results: int = 10) -> List[Dict[str, Any]]:
17
- """
18
- Finds relevant YouTube Shorts for a given movie title using YouTube Data API v3.
 
19
 
20
- Args:
21
- movie_title (str): The title of the movie or TV series.
22
- order (str): The order to sort the search results. Can be "relevance", "viewCount", or "rating".
23
- max_results (int): The maximum number of shorts to return.
 
 
 
 
 
24
 
25
- Returns:
26
- list: A list of dictionaries, each representing a found YouTube Short
27
- with its title, link, video_id, channel, and published_at.
28
- """
29
- if not GOOGLE_API_KEY:
30
- return []
 
 
 
 
 
 
 
 
31
 
32
- youtube = build("youtube", "v3", developerKey=GOOGLE_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- # Refined search query: More focused on movie title and direct shorts indicators.
35
- # We'll rely more on post-filtering for excluding long-form.
36
- # The query now explicitly requests "#shorts" or "YouTube Shorts" alongside the movie title.
37
- search_query = f'"{movie_title}" #shorts OR "{movie_title}" "YouTube Shorts"'
38
 
 
 
 
39
  try:
40
- request = youtube.search().list(
41
- q=search_query,
42
- part="snippet",
43
- type="video",
44
- videoDuration="short", # Filters videos under 4 minutes
45
- order=order,
46
- maxResults=max_results * 5, # Fetch more to allow for aggressive filtering
47
- )
48
- response = request.execute()
49
-
50
- potential_shorts = []
51
- for item in response.get('items', []):
52
- if item['id']['kind'] == 'youtube#video':
53
- video_id = item['id']['videoId']
54
- snippet = item['snippet']
55
-
56
- potential_shorts.append({
57
- "title": snippet['title'],
58
- "link": f"https://www.youtube.com/watch?v={video_id}",
59
- "video_id": video_id,
60
- "channel_title": snippet['channelTitle'],
61
- "published_at": snippet['publishedAt'],
62
- "description": snippet['description'],
63
- "thumbnails": snippet['thumbnails']['default']['url'],
64
- })
65
-
66
- # Stronger post-API filtering
67
- final_shorts = []
68
- # Indicators that strongly suggest long-form content
69
- long_form_indicators = [
70
- "full movie", "full episode", "season", "documentary",
71
- "trailer review", "official trailer", "compilation", "movie review",
72
- "best scenes", "explained", "summary", "recap", "analysis", "episodes",
73
- "movie in 10 minutes", "full story", "watch full"
74
- ]
75
- # Strong indicators that it is a short video
76
- shorts_indicators = ["#shorts", "youtube shorts", "short video", "short clip", "vertical video"]
77
-
78
- movie_title_lower = movie_title.lower()
79
-
80
- for short in potential_shorts:
81
- title_lower = short['title'].lower()
82
- description_lower = short['description'].lower()
83
-
84
- # Rule 1: Must contain a strong shorts indicator
85
- has_strong_shorts_indicator = any(indicator in title_lower or indicator in description_lower for indicator in shorts_indicators)
86
-
87
- # Rule 2: Ensure movie title (or part of it) is present in title/description for strong relevance
88
- # Using split for multi-word titles to check for partial matches too
89
- is_relevant_to_movie = movie_title_lower in title_lower or movie_title_lower in description_lower or \
90
- any(word in title_lower or word in description_lower for word in movie_title_lower.split())
91
-
92
-
93
- # Rule 3: Must NOT contain strong long-form indicators
94
- is_not_long_form = not any(indicator in title_lower or indicator in description_lower for indicator in long_form_indicators)
95
-
96
- if has_strong_shorts_indicator and is_relevant_to_movie and is_not_long_form:
97
- final_shorts.append(short)
98
-
99
- if len(final_shorts) >= max_results:
100
- break
101
-
102
- return final_shorts
103
 
 
 
 
 
104
  except Exception as e:
105
- print(f"An error occurred during YouTube Data API search for '{movie_title}': {e}")
106
  return []
107
 
108
- @app.get("/get_shorts_for_movie")
109
- async def get_shorts_for_movie(
110
- movie_title: str = Query(..., description="Title of the movie or TV series"),
111
- search_order: Optional[str] = Query("relevance", description="Order by relevance, viewCount, or rating"),
112
- max_shorts: Optional[int] = Query(10, description="Maximum number of shorts to return")
113
- ):
114
- if not movie_title:
115
- raise HTTPException(status_code=400, detail="'movie_title' query parameter is required.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- if not GOOGLE_API_KEY:
118
- raise HTTPException(status_code=500, detail="Google API Key is not configured.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- relevant_shorts = find_relevant_youtube_shorts(movie_title, order=search_order, max_results=max_shorts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- return {"movie_title": movie_title, "related_shorts": relevant_shorts}
 
 
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)