mtoft20 commited on
Commit
f8d43f8
Β·
verified Β·
1 Parent(s): 24f2ad6

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +705 -368
src/streamlit_app.py CHANGED
@@ -1,413 +1,750 @@
1
  import streamlit as st
2
  import requests
3
- import pandas as pd
4
- from together import Together
5
- import os
 
6
 
7
- # =============================================================================
8
- # CONFIGURATION - Using Secrets Management
9
- # =============================================================================
10
- NOCODB_URL = "https://app.nocodb.com" # Base URL
11
 
12
- # Get sensitive data from Streamlit secrets or environment variables
13
- def get_api_credentials():
14
- """Get API credentials from secrets or environment"""
15
- try:
16
- # Try Streamlit secrets first (for Hugging Face Spaces)
17
- api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", ""))
18
- together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", ""))
19
- endpoint_path = st.secrets.get("NOCODB_ENDPOINT_PATH", os.environ.get("NOCODB_ENDPOINT_PATH", ""))
20
-
21
- return api_token, together_key, endpoint_path
22
- except:
23
- # Fallback to environment variables
24
- api_token = os.environ.get("NOCODB_API_TOKEN", "")
25
- together_key = os.environ.get("TOGETHER_API_KEY", "")
26
- endpoint_path = os.environ.get("NOCODB_ENDPOINT_PATH", "")
27
-
28
- return api_token, together_key, endpoint_path
29
 
30
- # Initialize Together AI client
31
- @st.cache_resource
32
- def get_ai_client():
33
- """Initialize Together AI client"""
34
- _, together_key, _ = get_api_credentials()
35
- if not together_key:
36
- st.error("Together AI API key not found. Please configure it in the secrets.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  return None
38
- return Together(api_key=together_key)
39
 
40
- # =============================================================================
41
- # HELPER FUNCTIONS
42
- # =============================================================================
43
- @st.cache_data(ttl=300) # Cache for 5 minutes
44
- def get_netflix_content():
45
- """Fetch Netflix content from NocoDB"""
46
- api_token, _, endpoint_path = get_api_credentials()
47
-
48
- if not api_token or not endpoint_path:
49
- st.error("NocoDB credentials not configured. Please set up your secrets.")
50
  return []
51
-
52
- headers = {"xc-token": api_token}
53
-
54
  try:
55
- response = requests.get(
56
- f"{NOCODB_URL}{endpoint_path}?limit=10000",
57
- headers=headers
58
- )
59
 
60
- if response.status_code == 200:
61
- data = response.json()
62
- return data.get('list', [])
63
- else:
64
- st.error(f"Failed to fetch data: {response.status_code}")
65
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  except Exception as e:
68
- st.error(f"Error connecting to database: {e}")
 
 
69
  return []
70
 
71
- def filter_content(content_list, filters):
72
- """Apply filters to Netflix content list"""
73
- filtered = []
74
-
75
- for content in content_list:
76
- # Type filter
77
- if filters['content_type'] and content.get('type') != filters['content_type']:
78
- continue
79
 
80
- # Rating filter
81
- if filters['ratings'] and content.get('rating') not in filters['ratings']:
82
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- # Genre filter
85
- if filters['genres'] and not any(genre in content.get('listed_in', '') for genre in filters['genres']):
86
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- # Release year filter
89
- try:
90
- release_year = int(content.get('release_year', 0))
91
- if release_year < filters['year_range'][0] or release_year > filters['year_range'][1]:
92
  continue
93
- except (ValueError, TypeError):
94
- continue
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- # Duration filter (different handling for movies and shows)
97
- duration = content.get('duration', '')
98
- if filters['content_type'] == 'Movie' and 'min' in duration:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  try:
100
- minutes = int(duration.split()[0])
101
- if minutes < filters['duration_range'][0] or minutes > filters['duration_range'][1]:
 
102
  continue
103
- except (ValueError, IndexError):
 
 
 
 
 
 
 
 
 
 
 
 
104
  continue
105
 
106
- filtered.append(content)
107
-
108
- return filtered
109
 
110
- def create_content_context(content_list):
111
- """Create context string about current content for AI"""
112
- if not content_list:
113
- return "No content matches the current filters."
114
-
115
- total = len(content_list)
116
- movies = sum(1 for c in content_list if c.get('type') == 'Movie')
117
- shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
118
-
119
- context = f"""Currently showing {total} Netflix titles ({movies} movies and {shows} TV shows). """
120
-
121
- # Add some genre info
122
- all_genres = []
123
- for content in content_list[:20]: # Sample from first 20 items
124
- genres = content.get('listed_in', '').split(', ')
125
- all_genres.extend(genres)
126
-
127
- if all_genres:
128
- genre_counts = pd.Series(all_genres).value_counts()
129
- top_genres = genre_counts.head(5).index.tolist()
130
- context += f"Top genres include: {', '.join(top_genres)}. "
131
-
132
- # Add year range
133
- years = [int(c.get('release_year', 0)) for c in content_list if c.get('release_year')]
134
- if years:
135
- context += f"Release years range from {min(years)} to {max(years)}."
136
-
137
- return context
138
 
139
- def get_ai_response(client, question, context, model_name):
140
- """Get response from Together AI"""
141
  try:
142
- prompt = f"""You are a helpful Netflix content expert. Based on the current content data, please answer the user's question accurately and helpfully.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- Current Content Data Context:
145
- {context}
 
146
 
147
- User Question: {question}
 
 
 
 
148
 
149
- Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative."""
 
 
 
 
 
 
 
 
 
150
 
151
- response = client.chat.completions.create(
152
- model=model_name,
153
- messages=[
154
- {"role": "system", "content": "You are a helpful Netflix content expert with deep knowledge of movies and TV shows."},
155
- {"role": "user", "content": prompt}
156
- ],
157
- max_tokens=300,
158
- temperature=0.7,
159
- )
160
-
161
- return response.choices[0].message.content
162
-
163
- except Exception as e:
164
- raise Exception(f"Together AI Error: {str(e)}")
165
 
166
- # =============================================================================
167
- # MAIN APP
168
- # =============================================================================
169
- def main():
170
- # Page config
171
- st.set_page_config(
172
- page_title="Netflix Content Explorer",
173
- page_icon="🎬",
174
- layout="wide"
175
- )
176
-
177
- # Header
178
- st.title("🎬 Netflix Content Explorer")
179
- st.write("Explore Netflix movies and TV shows with AI-powered insights!")
180
-
181
- # Check API credentials
182
- api_token, together_key, endpoint_path = get_api_credentials()
183
-
184
- if not together_key:
185
- st.error("⚠️ Together AI API key not configured!")
186
- st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
187
- st.stop()
188
-
189
- if not api_token or not endpoint_path:
190
- st.error("⚠️ NocoDB credentials not configured!")
191
- st.info("Please set NOCODB_API_TOKEN and NOCODB_ENDPOINT_PATH in the Hugging Face Spaces secrets.")
192
- st.stop()
193
-
194
- # Initialize AI client
195
- try:
196
- client = get_ai_client()
197
- if not client:
198
- st.stop()
199
- except Exception as e:
200
- st.error(f"Failed to initialize Together AI client: {e}")
201
- st.stop()
202
-
203
- # Load all content first
204
- with st.spinner("Loading Netflix content..."):
205
- all_content = get_netflix_content()
206
 
207
- if not all_content:
208
- st.error("Could not load Netflix content. Please check your NocoDB connection.")
209
- st.stop()
210
 
211
- # Extract unique values for filters
212
- all_ratings = sorted(list(set([c.get('rating') for c in all_content if c.get('rating')])))
213
- all_genres = sorted(list(set(
214
- genre.strip()
215
- for c in all_content
216
- for genre in c.get('listed_in', '').split(',')
217
- if genre.strip()
218
- )))
219
-
220
- # Sidebar filters
221
- st.sidebar.header("πŸ” Filter Content")
222
-
223
- content_type = st.sidebar.selectbox(
224
- "Content Type",
225
- options=["All", "Movie", "TV Show"],
226
- index=0
227
- )
228
-
229
- selected_ratings = st.sidebar.multiselect(
230
- "Ratings",
231
- options=all_ratings,
232
- default=[]
233
- )
234
-
235
- selected_genres = st.sidebar.multiselect(
236
- "Genres",
237
- options=all_genres,
238
- default=[]
239
- )
240
-
241
- # Year range slider
242
- years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
243
- min_year, max_year = min(years), max(years)
244
- year_range = st.sidebar.slider(
245
- "Release Year",
246
- min_value=min_year,
247
- max_value=max_year,
248
- value=(min_year, max_year)
249
- )
250
-
251
- # Duration range slider (for movies only)
252
- movie_durations = [
253
- int(c.get('duration', '0 min').split()[0])
254
- for c in all_content
255
- if c.get('type') == 'Movie' and 'min' in c.get('duration', '')
256
- ]
257
- if movie_durations:
258
- min_duration, max_duration = min(movie_durations), max(movie_durations)
259
- duration_range = st.sidebar.slider(
260
- "Movie Duration (minutes)",
261
- min_value=min_duration,
262
- max_value=max_duration,
263
- value=(min_duration, max_duration),
264
- help="This filter only applies to movies"
265
- )
266
- else:
267
- duration_range = (0, 1000) # Fallback values
268
-
269
- # Create filter dictionary
270
- filters = {
271
- 'content_type': content_type if content_type != "All" else None,
272
- 'ratings': selected_ratings,
273
- 'genres': selected_genres,
274
- 'year_range': year_range,
275
- 'duration_range': duration_range
276
- }
277
-
278
- # Apply filters
279
- filtered_content = filter_content(all_content, filters)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
- # Main content area
282
- col1, col2 = st.columns([2, 1])
 
 
 
 
283
 
284
- with col1:
285
- # Content listings
286
- st.subheader(f"πŸ“‹ Found {len(filtered_content)} Titles")
287
-
288
- if filtered_content:
289
- # Show first 10 items
290
- for i, content in enumerate(filtered_content[:10]):
291
- with st.expander(f"{content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})"):
292
- # Content details in columns
293
- detail_col1, detail_col2 = st.columns(2)
294
-
295
- with detail_col1:
296
- st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
297
- st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
298
- st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
299
-
300
- with detail_col2:
301
- st.write(f"**🎬 Genre:** {content.get('listed_in', 'N/A')}")
302
- # Fix cast handling to prevent NoneType error
303
- cast = content.get('cast')
304
- cast_display = cast[:100] + "..." if cast else "N/A"
305
- st.write(f"**πŸ‘₯ Cast:** {cast_display}")
306
- st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
307
-
308
- # Description
309
- st.write(f"**πŸ“– Description:**")
310
- st.write(content.get('description', 'N/A'))
311
-
312
- if len(filtered_content) > 10:
313
- st.info(f"Showing first 10 of {len(filtered_content)} titles. Adjust filters to narrow results.")
314
- else:
315
- st.info("No content matches your current filters. Try adjusting the criteria.")
316
 
317
- with col2:
318
- # AI Chat Section
319
- st.subheader("πŸ€– Ask AI Assistant")
320
- st.write("Ask questions about the Netflix content!")
321
-
322
- # Model selection for Together AI
323
- model_choice = st.selectbox(
324
- "Select AI Model:",
325
- [
326
- "google/gemma-2b-it",
327
- "google/gemma-2-27b-it",
328
- "mistralai/Mistral-7B-Instruct-v0.1",
329
- "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
330
- "mistralai/Mixtral-8x7B-Instruct-v0.1"
331
- ],
332
- help="Select an AI model to answer your questions"
333
- )
334
 
335
- # Example questions
336
- with st.expander("πŸ’‘ Example Questions"):
337
- st.write("β€’ What are the most common genres?")
338
- st.write("β€’ What's the average movie duration?")
339
- st.write("β€’ Which directors have the most content?")
340
- st.write("β€’ What are the trends in content ratings?")
341
- st.write("β€’ Tell me about the release year distribution")
342
-
343
- user_question = st.text_area(
344
- "Your Question:",
345
- placeholder="Ask about genres, ratings, directors, trends...",
346
- height=100
347
- )
348
 
349
- if st.button("πŸ” Ask AI", type="primary"):
350
- if user_question:
351
- with st.spinner("AI is analyzing the content..."):
352
- # Create context from current filtered data
353
- context = create_content_context(filtered_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
- try:
356
- # Get AI response
357
- ai_response = get_ai_response(client, user_question, context, model_choice)
358
-
359
- st.success("**AI Assistant Response:**")
360
- st.write(ai_response)
361
-
362
- # Show debug info
363
- with st.expander("Debug Info"):
364
- st.write(f"Model used: {model_choice}")
365
- st.write(f"Content items analyzed: {len(filtered_content)}")
366
- st.write(f"Context: {context[:150]}...")
367
 
368
- except Exception as e:
369
- st.error(f"AI Error: {str(e)}")
 
 
370
 
371
- # Fallback response with data analysis
372
- st.info("**Fallback Analysis:**")
373
- if filtered_content:
374
- movies = sum(1 for c in filtered_content if c.get('type') == 'Movie')
375
- shows = sum(1 for c in filtered_content if c.get('type') == 'TV Show')
376
- st.write(f"β€’ Found {len(filtered_content)} titles ({movies} movies, {shows} TV shows)")
377
-
378
- # Show top genres
379
- genres = [g.strip() for c in filtered_content for g in c.get('listed_in', '').split(',')]
380
- genre_counts = pd.Series(genres).value_counts()
381
- st.write(f"β€’ Top genres: {', '.join(genre_counts.head(3).index)}")
382
-
383
- # Show year range
384
- years = [int(c.get('release_year', 0)) for c in filtered_content if c.get('release_year')]
385
- if years:
386
- st.write(f"β€’ Release years: {min(years)} - {max(years)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  else:
388
- st.warning("Please enter a question first!")
389
 
390
- # Footer stats
391
- st.markdown("---")
392
- if all_content:
393
- total_items = len(all_content)
394
- filtered_items = len(filtered_content)
395
-
396
- stat_col1, stat_col2, stat_col3, stat_col4 = st.columns(4)
397
-
398
- with stat_col1:
399
- st.metric("Total Titles", total_items)
400
-
401
- with stat_col2:
402
- st.metric("Filtered Results", filtered_items)
403
-
404
- with stat_col3:
405
- movies = sum(1 for c in filtered_content if c.get('type') == 'Movie')
406
- st.metric("Movies", movies)
407
 
408
- with stat_col4:
409
- shows = sum(1 for c in filtered_content if c.get('type') == 'TV Show')
410
- st.metric("TV Shows", shows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
412
- if __name__ == "__main__":
413
- main()
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import requests
3
+ import hashlib
4
+ import datetime
5
+ import uuid
6
+ from datetime import timedelta
7
 
8
+ # --- Configuration (Replace with your NocoDB details) ---
9
+ NOCODB_API_URL = "https://mtoft20-potm.hf.space/api/v1/db/data/noco/pt374k79jaxk7jr" # API endpoint URL
10
+ NOCODB_API_TOKEN = "-4o0sHFbhvIOZT356Wgtgfb0-JpQc8fz_w_rHUji" # Your NocoDB API token
 
11
 
12
+ # --- Helper Functions for NocoDB Interaction ---
13
+ def get_nocodb_headers():
14
+ return {
15
+ "xc-token": NOCODB_API_TOKEN,
16
+ "Content-Type": "application/json",
17
+ "Accept": "application/json"
18
+ }
 
 
 
 
 
 
 
 
 
 
19
 
20
+ def get_active_match():
21
+ """Get the currently active match"""
22
+ try:
23
+ response = requests.get(
24
+ f"{NOCODB_API_URL}/matches?where=(status,eq,active)",
25
+ headers=get_nocodb_headers()
26
+ )
27
+ response.raise_for_status()
28
+ matches = response.json().get("list", [])
29
+
30
+ if matches and matches[0].get('Id'):
31
+ try:
32
+ # Verify that the match has a valid integer ID
33
+ match = matches[0]
34
+ match['Id'] = int(match['Id'])
35
+ return match
36
+ except (ValueError, TypeError):
37
+ return None
38
+ return None
39
+ except requests.exceptions.RequestException as e:
40
+ st.error(f"Error fetching active match: {e}")
41
  return None
 
42
 
43
+ def get_all_players():
44
+ """Get all players from the players table"""
45
+ try:
46
+ response = requests.get(f"{NOCODB_API_URL}/players", headers=get_nocodb_headers())
47
+ response.raise_for_status()
48
+ return response.json().get("list", [])
49
+ except requests.exceptions.RequestException as e:
50
+ st.error(f"Error fetching players: {e}")
 
 
51
  return []
52
+
53
+ def get_players_for_match(match_id, debug_mode=True):
54
+ """Fetch players assigned to a specific match via match_players table, with error handling and debug info."""
55
  try:
56
+ if debug_mode:
57
+ st.write(f"Fetching players for match_id: {match_id}")
 
 
58
 
59
+ # Get match_players records
60
+ url = f"{NOCODB_API_URL}/match_players?where=(match_id,eq,{match_id})"
61
+ if debug_mode:
62
+ st.write(f"Querying match_players with URL: {url}")
63
+
64
+ resp = requests.get(url, headers=get_nocodb_headers())
65
+ resp.raise_for_status()
66
+ match_players = resp.json().get("list", [])
67
+
68
+ if debug_mode:
69
+ st.write(f"Raw match_players response: {match_players}")
70
+ # Print each match_player record structure
71
+ for idx, mp in enumerate(match_players):
72
+ st.write(f"Match player {idx} structure:")
73
+ st.write(mp)
74
+
75
+ # Extract player IDs, handling various possible field names
76
+ player_ids = []
77
+ for mp in match_players:
78
+ # Try all possible field variations
79
+ player_id = None
80
+ # Direct fields
81
+ if mp.get('player_id'):
82
+ player_id = mp['player_id']
83
+ elif mp.get('Id'):
84
+ player_id = mp['Id']
85
+ elif mp.get('id'):
86
+ player_id = mp['id']
87
+ # Nested fields (in case it's a relation)
88
+ elif isinstance(mp.get('player'), dict):
89
+ player_id = mp['player'].get('id') or mp['player'].get('Id')
90
+
91
+ if debug_mode:
92
+ st.write(f"Extracted player_id from record: {player_id}")
93
 
94
+ if player_id and str(player_id).isdigit() and int(player_id) > 0:
95
+ player_ids.append(int(player_id))
96
+
97
+ if debug_mode:
98
+ st.write(f"Final extracted player_ids: {player_ids}")
99
+
100
+ if not player_ids:
101
+ if debug_mode:
102
+ st.warning(f"No valid player IDs found for match {match_id}")
103
+ # Get all match_players for debugging
104
+ all_mp_url = f"{NOCODB_API_URL}/match_players"
105
+ all_mp_resp = requests.get(all_mp_url, headers=get_nocodb_headers())
106
+ if all_mp_resp.ok:
107
+ all_mp_data = all_mp_resp.json().get("list", [])
108
+ st.write("All match_players records:", all_mp_data)
109
+ # Analyze each record's structure
110
+ for mp in all_mp_data:
111
+ st.write("Record structure:", {k: type(v).__name__ for k, v in mp.items()})
112
+ return []
113
+
114
+ # Get player details
115
+ ids_str = ",".join(str(pid) for pid in player_ids)
116
+ players_url = f"{NOCODB_API_URL}/players?where=(Id,in,[{ids_str}])"
117
+
118
+ if debug_mode:
119
+ st.write(f"Querying players with URL: {players_url}")
120
+
121
+ resp = requests.get(players_url, headers=get_nocodb_headers())
122
+ resp.raise_for_status()
123
+ players = resp.json().get("list", [])
124
+
125
+ if debug_mode:
126
+ st.write(f"Retrieved players: {players}")
127
+
128
+ # Normalize player IDs
129
+ for player in players:
130
+ if 'id' in player and 'Id' not in player:
131
+ player['Id'] = player['id']
132
+ elif 'Id' in player and 'id' not in player:
133
+ player['id'] = player['Id']
134
+
135
+ return players
136
  except Exception as e:
137
+ if debug_mode:
138
+ st.error(f"Error in get_players_for_match: {str(e)}")
139
+ st.exception(e)
140
  return []
141
 
142
+ def has_user_voted(match_id, user_ip_hash):
143
+ """Check if user has already voted for this match"""
144
+ try:
145
+ # Ensure match_id is an integer
146
+ match_id = int(match_id) if match_id else None
147
+ if not match_id:
148
+ return False
 
149
 
150
+ response = requests.get(
151
+ f"{NOCODB_API_URL}/votes?where=(match_id,eq,{match_id})~and(user_ip_hash,eq,{user_ip_hash})",
152
+ headers=get_nocodb_headers()
153
+ )
154
+ response.raise_for_status()
155
+ votes = response.json().get("list", [])
156
+ return len(votes) > 0
157
+ except requests.exceptions.RequestException as e:
158
+ st.error(f"Error checking if user voted: {e}")
159
+ return False
160
+
161
+ def record_vote(match_id, player_id, user_ip_hash):
162
+ """Record a vote for a player in a match"""
163
+ try:
164
+ # Ensure match_id and player_id are integers
165
+ match_id = int(match_id) if match_id else None
166
+ player_id = int(player_id) if player_id else None
167
+
168
+ if not match_id or not player_id:
169
+ return False
170
 
171
+ vote_payload = {
172
+ "match_id": match_id,
173
+ "player_id": player_id,
174
+ "user_ip_hash": user_ip_hash,
175
+ "vote_time": datetime.datetime.now().isoformat()
176
+ }
177
+
178
+ response = requests.post(
179
+ f"{NOCODB_API_URL}/votes",
180
+ headers=get_nocodb_headers(),
181
+ json=vote_payload
182
+ )
183
+ response.raise_for_status()
184
+ return True
185
+ except requests.exceptions.RequestException as e:
186
+ st.error(f"Error recording vote: {e}")
187
+ return False
188
+
189
+ def get_historical_potms():
190
+ """Get historical Player of the Match winners"""
191
+ try:
192
+ # Get completed matches
193
+ matches_response = requests.get(
194
+ f"{NOCODB_API_URL}/matches?where=(status,eq,completed)&sort=-match_date",
195
+ headers=get_nocodb_headers()
196
+ )
197
+ matches_response.raise_for_status()
198
+ completed_matches = matches_response.json().get("list", [])
199
+
200
+ historical_data = []
201
+ all_players = get_all_players()
202
+
203
+ for match in completed_matches:
204
+ match_id = match.get('Id')
205
+ if not match_id:
206
+ continue
207
+
208
+ # Get all votes for this match
209
+ votes_response = requests.get(
210
+ f"{NOCODB_API_URL}/votes?where=(match_id,eq,{match_id})",
211
+ headers=get_nocodb_headers()
212
+ )
213
+ votes_response.raise_for_status()
214
+ votes = votes_response.json().get("list", [])
215
 
216
+ if not votes:
 
 
 
217
  continue
218
+
219
+ # Count votes by player
220
+ vote_counts = {}
221
+ for vote in votes:
222
+ player_id = vote.get('player_id')
223
+ if player_id:
224
+ vote_counts[player_id] = vote_counts.get(player_id, 0) + 1
225
+
226
+ if not vote_counts:
227
+ continue
228
+
229
+ # Find player with most votes
230
+ winner_id = max(vote_counts.items(), key=lambda x: x[1])[0]
231
+ winner_votes = vote_counts[winner_id]
232
 
233
+ # Get player name
234
+ winner = next((p for p in all_players if p.get('id') == winner_id), None)
235
+
236
+ if winner:
237
+ historical_data.append({
238
+ 'match_name': match.get('name', f'Match vs {match.get("opponent", "Unknown")}'),
239
+ 'match_date': match.get('match_date'),
240
+ 'player_name': winner.get('name'),
241
+ 'votes': winner_votes,
242
+ 'total_votes': sum(vote_counts.values())
243
+ })
244
+
245
+ return historical_data
246
+ except requests.exceptions.RequestException as e:
247
+ st.error(f"Error fetching historical POTM data: {e}")
248
+ return []
249
+
250
+ def add_player(name):
251
+ """Add a new player to the database"""
252
+ try:
253
+ # Check if player already exists
254
+ existing_players = get_all_players()
255
+ if any(p.get('name', '').lower() == name.lower() for p in existing_players):
256
+ return False, "Player already exists"
257
+
258
+ player_payload = {"name": name}
259
+ response = requests.post(
260
+ f"{NOCODB_API_URL}/players",
261
+ headers=get_nocodb_headers(),
262
+ json=player_payload
263
+ )
264
+ response.raise_for_status()
265
+ return True, "Player added successfully"
266
+ except requests.exceptions.RequestException as e:
267
+ return False, f"Error adding player: {e}"
268
+
269
+ def add_match(name, opponent, match_date, selected_player_ids):
270
+ """Add a new match with selected players"""
271
+ try:
272
+ # Create match
273
+ match_payload = {
274
+ "name": name,
275
+ "opponent": opponent,
276
+ "match_date": match_date.isoformat(),
277
+ "status": "upcoming"
278
+ }
279
+
280
+ match_response = requests.post(
281
+ f"{NOCODB_API_URL}/matches",
282
+ headers=get_nocodb_headers(),
283
+ json=match_payload
284
+ )
285
+ match_response.raise_for_status()
286
+ match_data = match_response.json()
287
+
288
+ try:
289
+ match_id = int(match_data.get('Id'))
290
+ except (ValueError, TypeError):
291
+ return False, "Could not create match: Invalid match ID"
292
+
293
+ if not match_id:
294
+ return False, "Could not create match: No match ID returned"
295
+
296
+ # Add players to match
297
+ success_count = 0
298
+ for player_id in selected_player_ids:
299
  try:
300
+ # Ensure player_id is an integer
301
+ player_id = int(player_id) if player_id else None
302
+ if not player_id:
303
  continue
304
+
305
+ match_player_payload = {
306
+ "match_id": match_id,
307
+ "player_id": player_id
308
+ }
309
+ mp_response = requests.post(
310
+ f"{NOCODB_API_URL}/match_players",
311
+ headers=get_nocodb_headers(),
312
+ json=match_player_payload
313
+ )
314
+ mp_response.raise_for_status()
315
+ success_count += 1
316
+ except (ValueError, TypeError, requests.exceptions.RequestException):
317
  continue
318
 
319
+ return True, f"Match created with {success_count} players"
320
+ except requests.exceptions.RequestException as e:
321
+ return False, f"Error creating match: {e}"
322
 
323
+ def update_match_status(match_id, status):
324
+ """Update match status"""
325
+ try:
326
+ payload = {"status": status}
327
+ response = requests.patch(
328
+ f"{NOCODB_API_URL}/matches/{match_id}",
329
+ headers=get_nocodb_headers(),
330
+ json=payload
331
+ )
332
+ response.raise_for_status()
333
+ return True
334
+ except requests.exceptions.RequestException:
335
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
+ def cleanup_invalid_match_players():
338
+ """Remove invalid match_players records with match_id=0 or player_id=0"""
339
  try:
340
+ # Get all invalid records
341
+ response = requests.get(
342
+ f"{NOCODB_API_URL}/match_players?where=(match_id,eq,0)~or(player_id,eq,0)",
343
+ headers=get_nocodb_headers()
344
+ )
345
+ response.raise_for_status()
346
+ invalid_records = response.json().get("list", [])
347
+
348
+ deleted_count = 0
349
+ for record in invalid_records:
350
+ try:
351
+ delete_response = requests.delete(
352
+ f"{NOCODB_API_URL}/match_players/{record['Id']}",
353
+ headers=get_nocodb_headers()
354
+ )
355
+ delete_response.raise_for_status()
356
+ deleted_count += 1
357
+ except requests.exceptions.RequestException:
358
+ continue
359
+ return True, f"Deleted {deleted_count} invalid match_players records"
360
+ except requests.exceptions.RequestException as e:
361
+ return False, f"Error cleaning up invalid records: {e}"
362
 
363
+ # --- Main App Logic ---
364
+ st.set_page_config(page_title="Player of the Match Voting", page_icon="⚽", layout="wide")
365
+ st.title("⚽ Player of the Match Voting App")
366
 
367
+ # Generate user identifier for voting uniqueness
368
+ if 'user_id' not in st.session_state:
369
+ st.session_state.user_id = str(uuid.uuid4())
370
+ user_identifier = st.session_state.user_id
371
+ user_ip_hash = hashlib.sha256(user_identifier.encode()).hexdigest()
372
 
373
+ # Check configuration
374
+ if NOCODB_API_URL == "YOUR_NOCODB_API_URL" or NOCODB_API_TOKEN == "YOUR_NOCODB_API_TOKEN":
375
+ st.error("⚠️ **Configuration Required**: Please update the NocoDB API URL and Token in the code.")
376
+ st.info("""
377
+ **Setup Instructions:**
378
+ 1. Replace `YOUR_NOCODB_API_URL` with your NocoDB API URL
379
+ 2. Replace `YOUR_NOCODB_API_TOKEN` with your NocoDB API token
380
+ 3. Set up the database structure as described in the documentation
381
+ """)
382
+ st.stop()
383
 
384
+ # Sidebar navigation
385
+ st.sidebar.header("Navigation")
386
+ view_mode = st.sidebar.radio(
387
+ "Select Mode:",
388
+ ["πŸ—³οΈ Vote for POTM", "πŸ“Š View Past POTMs", "βš™οΈ Developer Mode"]
389
+ )
 
 
 
 
 
 
 
 
390
 
391
+ # Developer Mode
392
+ if view_mode == "βš™οΈ Developer Mode":
393
+ st.header("πŸ”§ Developer Mode")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
+ # Password protection
396
+ dev_password = st.text_input("Enter Developer Password:", type="password")
 
397
 
398
+ if dev_password == "dev123": # Change this password as needed
399
+ st.success("βœ… Developer access granted")
400
+
401
+ # Tabs for different admin functions
402
+ tab1, tab2, tab3 = st.tabs(["πŸ‘₯ Manage Players", "⚽ Manage Matches", "πŸ“Š Database Status"])
403
+
404
+ with tab1:
405
+ st.subheader("Add New Player")
406
+ new_player_name = st.text_input("Player Name:")
407
+ if st.button("Add Player", type="primary"):
408
+ if new_player_name.strip():
409
+ success, message = add_player(new_player_name.strip())
410
+ if success:
411
+ st.success(message)
412
+ st.rerun()
413
+ else:
414
+ st.error(message)
415
+ else:
416
+ st.warning("Please enter a player name")
417
+
418
+ # Show existing players
419
+ st.subheader("Existing Players")
420
+ players = get_all_players()
421
+ if players:
422
+ for i, player in enumerate(players, 1):
423
+ player_id = player.get('Id') or 'Unknown'
424
+ st.write(f"{i}. {player.get('name')} (ID: {player_id})")
425
+ else:
426
+ st.info("No players found")
427
+
428
+ with tab2:
429
+ st.subheader("Add New Match")
430
+
431
+ col1, col2 = st.columns(2)
432
+ with col1:
433
+ match_name = st.text_input("Match Name:")
434
+ opponent = st.text_input("Opponent:")
435
+ with col2:
436
+ match_date = st.date_input("Match Date:", datetime.date.today())
437
+ match_time = st.time_input("Match Time:", datetime.time(19, 0))
438
+
439
+ # Player selection
440
+ players = get_all_players()
441
+ if players:
442
+ player_options = {f"{p.get('name')} (ID: {p.get('Id') or 'Unknown'})": p.get('Id') for p in players}
443
+ selected_players = st.multiselect(
444
+ "Select Players for this Match:",
445
+ options=list(player_options.keys())
446
+ )
447
+ selected_player_ids = [player_options[p] for p in selected_players if player_options[p] is not None]
448
+
449
+ if st.button("Create Match", type="primary"):
450
+ if match_name and opponent and selected_player_ids:
451
+ match_datetime = datetime.datetime.combine(match_date, match_time)
452
+ success, message = add_match(match_name, opponent, match_datetime, selected_player_ids)
453
+ if success:
454
+ st.success(message)
455
+ st.rerun()
456
+ else:
457
+ st.error(message)
458
+ else:
459
+ st.warning("Please fill all fields and select players")
460
+ else:
461
+ st.warning("No players available. Add players first.")
462
+
463
+ # Show existing matches
464
+ st.subheader("Existing Matches")
465
+ try:
466
+ matches_response = requests.get(
467
+ f"{NOCODB_API_URL}/matches?sort=-match_date",
468
+ headers=get_nocodb_headers()
469
+ )
470
+ matches_response.raise_for_status()
471
+ matches = matches_response.json().get("list", [])
472
+ if matches:
473
+ for match in matches:
474
+ status_emoji = {"upcoming": "πŸ”œ", "active": "πŸ”΄", "completed": "βœ…"}
475
+ emoji = status_emoji.get(match.get('status'), "❓")
476
+ col1, col2, col3 = st.columns([3, 1, 1])
477
+ with col1:
478
+ st.write(f"{emoji} **{match.get('name')}** vs {match.get('opponent')}")
479
+ match_date = match.get('match_date', 'N/A')
480
+ if match_date is not None:
481
+ match_date = match_date[:10]
482
+ st.caption(f"Date: {match_date} | Status: {match.get('status')}")
483
+ # --- EDIT MATCH PLAYERS ---
484
+ # Only allow editing for upcoming or active matches
485
+ if match.get('status') in ['upcoming', 'active']:
486
+ with st.expander("Edit Players", expanded=False):
487
+ try:
488
+ # Get all players
489
+ all_players = get_all_players()
490
+ # Get current players for this match
491
+ current_players = get_players_for_match(match.get('Id'), debug_mode=False)
492
+
493
+ # Create options dictionary with normalized IDs
494
+ player_options = {}
495
+ for p in all_players:
496
+ player_id = p.get('Id') or p.get('id')
497
+ if player_id:
498
+ player_options[f"{p.get('name')} (ID: {player_id})"] = int(player_id)
499
+
500
+ # Get current player display names
501
+ current_player_displays = []
502
+ for p in current_players:
503
+ player_id = p.get('Id') or p.get('id')
504
+ if player_id:
505
+ display = f"{p.get('name')} (ID: {player_id})"
506
+ if display in player_options:
507
+ current_player_displays.append(display)
508
+
509
+ selected_players = st.multiselect(
510
+ f"Select Players for {match.get('name')}",
511
+ options=list(player_options.keys()),
512
+ default=current_player_displays,
513
+ key=f"edit_players_{match.get('Id')}"
514
+ )
515
+
516
+ if st.button("Update Players", key=f"update_players_{match.get('Id')}"):
517
+ try:
518
+ # Delete existing match_players
519
+ mp_response = requests.get(
520
+ f"{NOCODB_API_URL}/match_players?where=(match_id,eq,{match.get('Id')})",
521
+ headers=get_nocodb_headers()
522
+ )
523
+ mp_response.raise_for_status()
524
+ match_players = mp_response.json().get("list", [])
525
+
526
+ for mp in match_players:
527
+ mp_id = mp.get('Id') or mp.get('id')
528
+ if mp_id:
529
+ delete_response = requests.delete(
530
+ f"{NOCODB_API_URL}/match_players/{mp_id}",
531
+ headers=get_nocodb_headers()
532
+ )
533
+ delete_response.raise_for_status()
534
+
535
+ # Add new match_players
536
+ selected_player_ids = [player_options[p] for p in selected_players]
537
+ success_count = 0
538
+
539
+ for player_id in selected_player_ids:
540
+ add_response = requests.post(
541
+ f"{NOCODB_API_URL}/match_players",
542
+ headers=get_nocodb_headers(),
543
+ json={
544
+ "match_id": int(match.get('Id')),
545
+ "player_id": int(player_id)
546
+ }
547
+ )
548
+ add_response.raise_for_status()
549
+ success_count += 1
550
+
551
+ st.success(f"βœ… Successfully updated {success_count} players!")
552
+ st.rerun()
553
+
554
+ except requests.exceptions.RequestException as e:
555
+ st.error(f"API Error: {str(e)}")
556
+ except Exception as e:
557
+ st.error(f"Unexpected error: {str(e)}")
558
+ except Exception as e:
559
+ st.error(f"Error loading player data: {str(e)}")
560
+ with col2:
561
+ if match.get('status') == 'upcoming':
562
+ if st.button("Activate", key=f"activate_{match.get('Id')}"):
563
+ if update_match_status(match.get('Id'), 'active'):
564
+ st.success("Match activated!")
565
+ st.rerun()
566
+ with col3:
567
+ if match.get('status') == 'active':
568
+ if st.button("Complete", key=f"complete_{match.get('Id')}"):
569
+ if update_match_status(match.get('Id'), 'completed'):
570
+ st.success("Match completed!")
571
+ st.rerun()
572
+ else:
573
+ st.info("No matches found")
574
+ except requests.exceptions.RequestException as e:
575
+ st.error(f"Error fetching matches: {e}")
576
+
577
+ with tab3:
578
+ st.subheader("Database Connection Status")
579
+ try:
580
+ # Test connection
581
+ test_response = requests.get(
582
+ f"{NOCODB_API_URL}/players?limit=1",
583
+ headers=get_nocodb_headers()
584
+ )
585
+ test_response.raise_for_status()
586
+ st.success("βœ… Database connection successful")
587
+
588
+ # Show stats
589
+ players_count = len(get_all_players())
590
+ st.metric("Total Players", players_count)
591
+
592
+ except requests.exceptions.RequestException as e:
593
+ st.error(f"❌ Database connection failed: {e}")
594
 
595
+ elif dev_password:
596
+ st.error("❌ Incorrect password")
597
+
598
+ # Voting Mode
599
+ elif view_mode == "πŸ—³οΈ Vote for POTM":
600
+ st.header("πŸ—³οΈ Vote for Player of the Match")
601
 
602
+ active_match = get_active_match()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
 
604
+ if active_match:
605
+ match_id = active_match.get('Id')
606
+ match_name = active_match.get('name', 'Current Match')
607
+ opponent = active_match.get('opponent', 'Unknown')
 
 
 
 
 
 
 
 
 
 
 
 
 
608
 
609
+ st.success(f"**Current Active Match:** {match_name} vs {opponent}")
610
+
611
+ # Fetch players for the active match
612
+ match_players = get_players_for_match(match_id)
 
 
 
 
 
 
 
 
 
613
 
614
+ if match_players:
615
+ st.subheader("Vote for your Player of the Match:")
616
+ # Display players for voting (e.g., using radio buttons)
617
+ player_options = {p.get('name'): p.get('Id') for p in match_players}
618
+ selected_player_name = st.radio(
619
+ "Select a player:",
620
+ list(player_options.keys()),
621
+ key=f"vote_player_{match_id}"
622
+ )
623
+
624
+ if st.button("Submit Vote", key=f"submit_vote_{match_id}"):
625
+ selected_player_id = player_options.get(selected_player_name)
626
+ if selected_player_id:
627
+ # Check if user has already voted
628
+ if has_user_voted(match_id, user_ip_hash):
629
+ st.info("βœ… You have already voted for this match. Thank you for your participation!")
630
+ else:
631
+ # Record the vote
632
+ if record_vote(match_id, int(selected_player_id), user_ip_hash):
633
+ st.success("πŸ—³οΈ Vote submitted successfully!")
634
+ st.rerun()
635
+ else:
636
+ st.error("❌ Failed to submit vote.")
637
+ else:
638
+ st.warning("Please select a player to vote.")
639
+
640
+ else:
641
+ st.warning("⚠️ No players found for this match. Please contact the administrator.")
642
+
643
+ # Check if user has already voted
644
+ if has_user_voted(match_id, user_ip_hash):
645
+ st.info("βœ… You have already voted for this match. Thank you for your participation!")
646
+
647
+ # Show current vote counts (optional)
648
+ with st.expander("View Current Results"):
649
+ try:
650
+ votes_response = requests.get(
651
+ f"{NOCODB_API_URL}/votes?where=(match_id,eq,{match_id})",
652
+ headers=get_nocodb_headers()
653
+ )
654
+ votes_response.raise_for_status()
655
+ votes = votes_response.json().get("list", [])
656
 
657
+ if votes:
658
+ vote_counts = {}
659
+ all_players = get_all_players()
 
 
 
 
 
 
 
 
 
660
 
661
+ for vote in votes:
662
+ player_id = vote.get('player_id')
663
+ if player_id:
664
+ vote_counts[player_id] = vote_counts.get(player_id, 0) + 1
665
 
666
+ st.write("**Current Vote Counts:**")
667
+ for player_id, count in sorted(vote_counts.items(), key=lambda x: x[1], reverse=True):
668
+ player = next((p for p in all_players if p.get('Id') == player_id), None)
669
+ if player:
670
+ st.write(f"β€’ {player.get('name')}: {count} votes")
671
+ else:
672
+ st.write("No votes yet.")
673
+ except:
674
+ st.write("Could not load current results.")
675
+
676
+ else:
677
+ # Show voting interface
678
+ players = get_players_for_match(match_id, debug_mode=True)
679
+
680
+ if players:
681
+ st.write("**Select the Player of the Match:**")
682
+
683
+ # Create voting form
684
+ with st.form("voting_form"):
685
+ selected_player = st.radio(
686
+ "Players:",
687
+ options=[p.get('name') for p in players],
688
+ key="player_selection"
689
+ )
690
+
691
+ submit_vote = st.form_submit_button("πŸ—³οΈ Submit Vote", type="primary")
692
+
693
+ if submit_vote and selected_player:
694
+ # Find selected player ID
695
+ selected_player_obj = next((p for p in players if p.get('name') == selected_player), None)
696
+
697
+ if selected_player_obj:
698
+ if record_vote(match_id, int(selected_player_obj.get('Id')), user_ip_hash):
699
+ st.balloons()
700
+ st.success(f"πŸŽ‰ Vote recorded for {selected_player}! Thank you!")
701
+ st.rerun()
702
+ else:
703
+ st.error("Failed to record vote. Please try again.")
704
  else:
705
+ st.warning("⚠️ No players found for this match. Please contact the administrator.")
706
 
707
+ else:
708
+ st.info("πŸ“… There is no active match currently. Please check back later!")
709
+
710
+ # Historical POTMs Mode
711
+ elif view_mode == "πŸ“Š View Past POTMs":
712
+ st.header("πŸ“Š Historical Player of the Match Winners")
713
+
714
+ historical_data = get_historical_potms()
715
+
716
+ if historical_data:
717
+ st.write(f"**Total Completed Matches:** {len(historical_data)}")
 
 
 
 
 
 
718
 
719
+ for i, data in enumerate(historical_data, 1):
720
+ with st.container():
721
+ col1, col2, col3 = st.columns([3, 2, 1])
722
+
723
+ with col1:
724
+ st.subheader(f"πŸ† {data['match_name']}")
725
+ st.write(f"**Winner:** {data['player_name']}")
726
+
727
+ with col2:
728
+ st.metric("Votes Received", f"{data['votes']}/{data['total_votes']}")
729
+ percentage = (data['votes'] / data['total_votes']) * 100 if data['total_votes'] > 0 else 0
730
+ st.write(f"({percentage:.1f}% of votes)")
731
+
732
+ with col3:
733
+ if data['match_date']:
734
+ st.write(f"πŸ“… {data['match_date'][:10]}")
735
+
736
+ st.divider()
737
+
738
+ else:
739
+ st.info("πŸ“ No historical data available yet. Complete some matches to see results here!")
740
 
741
+ # Footer
742
+ st.markdown("---")
743
+ st.markdown(
744
+ """
745
+ <div style='text-align: center; color: #666;'>
746
+ Player of the Match Voting System | Powered by Streamlit & NocoDB
747
+ </div>
748
+ """,
749
+ unsafe_allow_html=True
750
+ )