mtoft20 commited on
Commit
5356916
Β·
verified Β·
1 Parent(s): 9647cd9

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +460 -588
src/streamlit_app.py CHANGED
@@ -6,232 +6,161 @@ import os
6
  import json
7
 
8
  # =============================================================================
9
- # CONFIGURATION - Using Secrets Management
10
  # =============================================================================
11
- NOCODB_URL = "https://mtoft20-potm.hf.space".strip() # Base URL, ensure no extra spaces
12
 
13
- # Get sensitive data from Streamlit secrets or environment variables
 
 
14
  def get_api_credentials():
15
- """Get API credentials from secrets or environment"""
16
- try:
17
- # Try Streamlit secrets first (for Hugging Face Spaces)
18
- api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", "")).strip()
19
- together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", "")).strip()
20
-
21
- # Get endpoints for content and similarities
22
- content_endpoint = st.secrets.get("NOCODB_CONTENT_ENDPOINT", os.environ.get("NOCODB_CONTENT_ENDPOINT", "")).strip()
23
- similarity_endpoints = [endpoint.strip() for endpoint in
24
- st.secrets.get("NOCODB_SIMILARITY_ENDPOINTS",
25
- os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "")).split(",")
26
- if endpoint.strip()]
27
-
28
- return api_token, together_key, content_endpoint, similarity_endpoints
29
- except:
30
- # Fallback to environment variables
31
- api_token = os.environ.get("NOCODB_API_TOKEN", "").strip()
32
- together_key = os.environ.get("TOGETHER_API_KEY", "").strip()
33
- content_endpoint = os.environ.get("NOCODB_CONTENT_ENDPOINT", "").strip()
34
- similarity_endpoints = [endpoint.strip() for endpoint in
35
- os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "").split(",")
36
- if endpoint.strip()]
37
-
38
- return api_token, together_key, content_endpoint, similarity_endpoints
39
 
40
- # Initialize Together AI client
41
  @st.cache_resource
42
  def get_ai_client():
43
- """Initialize Together AI client"""
44
- _, together_key, _, _ = get_api_credentials()
45
- if not together_key:
46
  st.error("Together AI API key not found. Please configure it in the secrets.")
47
  return None
48
- return Together(api_key=together_key)
49
 
50
  # =============================================================================
51
- # HELPER FUNCTIONS
52
  # =============================================================================
53
- @st.cache_data(ttl=300) # Cache for 5 minutes
 
 
 
 
 
 
 
 
 
 
54
  def get_streaming_content():
55
- """Fetch streaming content from NocoDB with pagination"""
56
- api_token, _, content_endpoint, _ = get_api_credentials()
57
 
58
- if not api_token or not content_endpoint:
59
  st.error("NocoDB credentials not configured. Please set up your secrets.")
60
  return []
61
 
62
  headers = {
63
- "xc-token": api_token,
64
  "accept": "application/json"
65
  }
66
 
67
  all_content = []
68
  page = 1
69
- page_size = 1000 # NocoDB default page size
70
-
71
- try:
72
- while True:
73
- offset = (page - 1) * page_size
74
- url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}?limit={page_size}&offset={offset}"
75
-
76
- response = requests.get(url, headers=headers)
77
-
78
- if response.status_code == 200:
79
- data = response.json()
80
- current_page_data = data.get('list', [])
81
-
82
- if not current_page_data: # No more data to fetch
83
- break
84
-
85
- all_content.extend(current_page_data)
86
-
87
- # Check if this is the last page
88
- page_info = data.get('pageInfo', {})
89
- if page_info.get('isLastPage', True):
90
- break
91
-
92
- page += 1
93
- else:
94
- st.error(f"Failed to fetch data: {response.status_code}")
95
- if not all_content: # Only return [] if we haven't fetched any data
96
- return []
97
- break # If we have some data, return what we've got
98
-
99
- return all_content
100
-
101
- except Exception as e:
102
- st.error(f"Error connecting to database: {str(e)}")
103
- st.write("Full error details:", e)
104
- return []
105
-
106
- def filter_content(content_list, filters):
107
- """Apply filters to streaming content list"""
108
- filtered = []
109
 
110
- for content in content_list:
111
- if not content or not isinstance(content, dict):
112
- continue
113
-
114
- matches_all_filters = True
115
 
116
- # Streaming service filter
117
- if filters['streaming_services']:
118
- if content.get('streaming_service') not in filters['streaming_services']:
119
- matches_all_filters = False
120
- continue
121
-
122
- # Type filter - only apply if not "All"
123
- if filters['content_type']:
124
- if content.get('type') != filters['content_type']:
125
- matches_all_filters = False
126
- continue
127
 
128
- # Genre filter - check if ALL selected genres are in the content's genres
129
- if filters['genres']:
130
- content_genres = set(g.strip().lower() for g in str(content.get('listed_in', '')).split(','))
131
- selected_genres = set(g.strip().lower() for g in filters['genres'])
132
-
133
- if not selected_genres.issubset(content_genres):
134
- matches_all_filters = False
135
- continue
136
-
137
- # Rating filter
138
- if filters['ratings']:
139
- rating = content.get('rating', '').strip()
140
- # Only compare if rating is a valid string and not a duration
141
- if not rating or not isinstance(rating, str) or rating.endswith('min'):
142
- matches_all_filters = False
143
- continue
144
- if rating not in filters['ratings']:
145
- matches_all_filters = False
146
- continue
147
-
148
- # Release year filter
149
- try:
150
- release_year = int(content.get('release_year', 0))
151
- if release_year < filters['year_range'][0] or release_year > filters['year_range'][1]:
152
- matches_all_filters = False
153
- continue
154
- except (ValueError, TypeError):
155
- matches_all_filters = False
156
- continue
157
 
158
- # Duration filter (different handling for movies)
159
- if filters['content_type'] == 'Movie':
160
- duration = str(content.get('duration', ''))
161
- if 'min' in duration:
162
- try:
163
- minutes = int(duration.split()[0])
164
- if minutes < filters['duration_range'][0] or minutes > filters['duration_range'][1]:
165
- matches_all_filters = False
166
- continue
167
- except (ValueError, IndexError):
168
- matches_all_filters = False
169
- continue
170
-
171
- # Director filter (optional)
172
- if filters['director']:
173
- director = str(content.get('director', '')).lower()
174
- if not any(name.strip().lower() in director for name in filters['director'].split(',')):
175
- matches_all_filters = False
176
- continue
177
-
178
- # Cast filter (optional)
179
- if filters['cast']:
180
- cast = str(content.get('cast', '')).lower()
181
- if not any(name.strip().lower() in cast for name in filters['cast'].split(',')):
182
- matches_all_filters = False
183
- continue
184
 
185
- if matches_all_filters:
186
- filtered.append(content)
 
 
187
 
188
- return filtered
189
 
190
- def create_content_context(content_list):
191
- """Create context string about current content for AI"""
192
- if not content_list:
193
- return "No content matches the current filters."
194
-
195
- total = len(content_list)
196
- movies = sum(1 for c in content_list if c.get('type') == 'Movie')
197
- shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
198
 
199
- context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows). """
 
 
200
 
201
- # Add some genre info
202
- all_genres = []
203
- for content in content_list[:20]: # Sample from first 20 items
204
- genres = content.get('listed_in', '').split(', ')
205
- all_genres.extend(genres)
206
 
207
- if all_genres:
208
- genre_counts = pd.Series(all_genres).value_counts()
209
- top_genres = genre_counts.head(5).index.tolist()
210
- context += f"Top genres include: {', '.join(top_genres)}. "
211
 
212
- # Add year range
213
- years = [int(c.get('release_year', 0)) for c in content_list if c.get('release_year')]
214
- if years:
215
- context += f"Release years range from {min(years)} to {max(years)}."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- return context
218
 
 
 
 
219
  def get_ai_response(client, question, context, model_name):
220
- """Get response from Together AI"""
221
  try:
222
- prompt = f"""You are a helpful streaming content expert. Based on the current content data, please answer the user's question accurately and helpfully.
 
223
 
224
  Current Content Data Context:
225
  {context}
226
 
227
  User Question: {question}
228
 
229
- Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative."""
230
 
231
  response = client.chat.completions.create(
232
  model=model_name,
233
  messages=[
234
- {"role": "system", "content": "You are a helpful content expert with deep knowledge of movies and TV shows."},
235
  {"role": "user", "content": prompt}
236
  ],
237
  max_tokens=300,
@@ -241,165 +170,101 @@ Please provide a helpful, accurate response based on the data provided. Keep you
241
  return response.choices[0].message.content
242
 
243
  except Exception as e:
244
- raise Exception(f"Together AI Error: {str(e)}")
245
-
246
- def extract_unique_names(content_list, field):
247
- """Extract unique names from a comma-separated field in content list"""
248
- unique_names = set()
249
- for content in content_list:
250
- names = content.get(field, '')
251
- if names:
252
- # Split by comma and clean each name
253
- for name in names.split(','):
254
- cleaned_name = name.strip()
255
- if cleaned_name: # Only add non-empty names
256
- unique_names.add(cleaned_name)
257
- return sorted(list(unique_names))
258
 
259
- def get_similar_content(content, n_recommendations=5):
260
- """Get pre-computed similar content from database"""
 
 
 
261
  try:
262
- # Get database credentials
263
- api_token, _, content_endpoint, similarity_endpoints = get_api_credentials()
264
-
265
- if not api_token or not similarity_endpoints:
266
- st.error("NocoDB credentials not configured properly.")
267
- return []
268
-
269
- headers = {
270
- "xc-token": api_token,
271
- "accept": "application/json"
272
- }
273
-
274
- title = content.get('title', '')
275
- show_id = content.get('show_id', '')
276
-
277
- # Try finding by both show_id and title
278
- query = f'where=(show_id,eq,{show_id})~and(title,eq,{title})'
279
- params = {
280
- "where": query
281
- }
282
-
283
- for endpoint in similarity_endpoints:
284
- if not endpoint.strip(): # Skip empty endpoints
285
- continue
286
-
287
- try:
288
- url = f"{NOCODB_URL.strip()}{endpoint.strip()}"
289
- response = requests.get(url, headers=headers, params=params)
290
-
291
- if response.status_code == 200:
292
- data = response.json()
293
- if data.get('list'):
294
- for entry in data['list']:
295
- try:
296
- similar_items = json.loads(entry['similar_items'])
297
-
298
- # Get full content details for each similar item
299
- similar_content = []
300
- for item in similar_items[:n_recommendations]:
301
- show_id = item.get('show_id', '')
302
- query = f'where=(show_id,eq,{show_id})'
303
- content_params = {
304
- "where": query
305
- }
306
- content_url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}"
307
- content_response = requests.get(content_url, headers=headers, params=content_params)
308
-
309
- if content_response.status_code == 200:
310
- content_data = content_response.json()
311
- if content_data and len(content_data.get('list', [])) > 0:
312
- content_dict = content_data['list'][0]
313
- content_dict['similarity'] = f"{item['similarity']:.2%}"
314
- similar_content.append(content_dict)
315
-
316
- return similar_content[:n_recommendations]
317
- except Exception as parse_error:
318
- continue
319
- except Exception as e:
320
- continue
321
-
322
- return []
323
  except Exception as e:
 
324
  return []
325
 
326
- # =============================================================================
327
- # MAIN APP
328
- # =============================================================================
329
- def main():
330
- # Page config
331
- st.set_page_config(
332
- page_title="StreamButler - Your Personal Streaming Concierge",
333
- page_icon="🎩",
334
- layout="wide"
335
- )
336
-
337
- # Header with butler theme
338
- st.title("🎩 StreamButler")
339
- st.write("*At your service! Allow me to curate the perfect streaming entertainment for you.*")
340
-
341
- # Check API credentials
342
- api_token, together_key, content_endpoint, similarity_endpoints = get_api_credentials()
343
-
344
- if not together_key:
345
- st.error("⚠️ Together AI API key not configured!")
346
- st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
347
- st.stop()
348
 
349
- if not api_token or not content_endpoint:
350
- st.error("⚠️ NocoDB credentials not configured!")
351
- st.info("Please set NOCODB_API_TOKEN and NOCODB_CONTENT_ENDPOINT in the Hugging Face Spaces secrets.")
352
- st.stop()
353
 
354
- # Initialize AI client
355
- try:
356
- client = get_ai_client()
357
- if not client:
358
- st.stop()
359
- except Exception as e:
360
- st.error(f"Failed to initialize Together AI client: {e}")
361
- st.stop()
362
 
363
- # Load all content first
364
- with st.spinner("Loading streaming content..."):
365
- all_content = get_streaming_content()
366
 
367
- if not all_content:
368
- st.error("Could not load streaming content. Please check your NocoDB connection.")
369
- st.stop()
 
 
 
 
 
370
 
371
- # Extract unique values for filters
372
  all_ratings = sorted(list(set(
373
  c.get('rating') for c in all_content
374
- if c and isinstance(c, dict)
375
- and c.get('rating')
376
- and isinstance(c.get('rating'), str)
377
- and not c.get('rating').endswith('min') # Exclude duration values
378
- and c.get('rating').strip() # Exclude empty strings
379
  )))
 
380
  all_genres = sorted(list(set(
381
  genre.strip()
382
  for c in all_content
383
- for genre in c.get('listed_in', '').split(',')
384
  if genre.strip()
385
  )))
386
- all_streaming_services = sorted(list(set([c.get('streaming_service') for c in all_content if c.get('streaming_service')])))
 
 
 
 
 
387
 
388
  # Extract unique directors and cast members
389
- all_directors = extract_unique_names(all_content, 'director')
390
- all_cast_members = extract_unique_names(all_content, 'cast')
 
 
 
 
 
 
 
 
 
 
 
391
 
392
- # Sidebar filters
393
- st.sidebar.header("πŸ” Filter Content")
394
-
395
  with st.sidebar.form("filter_form"):
 
396
  st.subheader("Streaming Services")
397
-
398
- # Streaming service selection (required)
399
  selected_services = st.multiselect(
400
  "Select Your Streaming Services",
401
  options=all_streaming_services,
402
- default=all_streaming_services[:1], # Default to first service
403
  help="Select the streaming services you have access to",
404
  key="streaming_services"
405
  )
@@ -407,31 +272,31 @@ def main():
407
  if not selected_services:
408
  st.warning("Please select at least one streaming service")
409
 
 
410
  st.subheader("Content Filters")
411
-
412
  content_type = st.selectbox(
413
  "Content Type",
414
  options=["All", "Movie", "TV Show"],
415
  index=0
416
  )
417
 
 
418
  selected_genres = st.multiselect(
419
  "Genres",
420
  options=all_genres,
421
  default=[]
422
  )
423
-
424
- st.subheader("Optional Filters")
425
 
426
- # Rating filter
 
427
  selected_ratings = st.multiselect(
428
  "Ratings",
429
  options=all_ratings,
430
  default=[],
431
  help="Filter by content rating"
432
  )
433
-
434
- # Year range slider
435
  years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
436
  min_year, max_year = min(years), max(years)
437
  year_range = st.slider(
@@ -442,13 +307,14 @@ def main():
442
  help="Filter by release year range"
443
  )
444
 
445
- # Duration range slider (for movies only)
446
  movie_durations = [
447
  int(str(c.get('duration', '0 min')).split()[0])
448
  for c in all_content
449
  if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
450
  ]
451
 
 
452
  if movie_durations:
453
  min_duration = min(d for d in movie_durations if d > 0)
454
  max_duration = max(movie_durations)
@@ -459,10 +325,8 @@ def main():
459
  value=(min_duration, max_duration),
460
  help="This filter only applies to movies"
461
  )
462
- else:
463
- duration_range = (0, 1000) # Fallback values
464
-
465
- # Director filter with autocomplete
466
  selected_directors = st.multiselect(
467
  "Directors",
468
  options=all_directors,
@@ -470,8 +334,7 @@ def main():
470
  help="Select one or more directors (searchable)",
471
  placeholder="Start typing to search directors..."
472
  )
473
-
474
- # Cast filter with autocomplete
475
  selected_cast = st.multiselect(
476
  "Cast Members",
477
  options=all_cast_members,
@@ -480,285 +343,294 @@ def main():
480
  placeholder="Start typing to search cast members..."
481
  )
482
 
483
- # Submit button
484
  apply_filters = st.form_submit_button("πŸ” Apply Filters", type="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
- # Create filter dictionary
487
- filters = {
488
- 'streaming_services': selected_services,
489
- 'content_type': content_type if content_type != "All" else None,
490
- 'ratings': selected_ratings,
491
- 'genres': selected_genres,
492
- 'year_range': year_range,
493
- 'duration_range': duration_range,
494
- 'director': ','.join(selected_directors) if selected_directors else '',
495
- 'cast': ','.join(selected_cast) if selected_cast else ''
496
- }
497
 
498
- # Only apply filters when the button is clicked
499
- if apply_filters:
500
- filtered_content = filter_content(all_content, filters)
501
- st.session_state.filtered_content = filtered_content
502
- else:
503
- # Initialize filtered content if not exists
504
- if 'filtered_content' not in st.session_state:
505
- st.session_state.filtered_content = all_content
506
-
507
- # Main content area
508
- col1, col2 = st.columns([2, 1])
509
 
510
- with col1:
511
- # Content listings
512
- filtered_count = len(st.session_state.filtered_content)
513
- if filtered_count == 0:
514
- st.subheader("πŸ“‹ No Titles Found")
515
- else:
516
- # Header with count and page info
517
- st.subheader(f"πŸ“‹ Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
518
-
519
- if st.session_state.filtered_content:
520
- # Active Filters section with better formatting
521
- if any([filters['content_type'], filters['genres'], filters['ratings'],
522
- filters['director'], filters['cast']]):
523
- with st.expander("πŸ” Active Filters", expanded=True):
524
- filter_cols = st.columns(2)
525
- with filter_cols[0]:
526
- if filters['content_type']:
527
- st.write(f"**Type:** {filters['content_type']}")
528
- if filters['genres']:
529
- st.write(f"**Genres:** {', '.join(filters['genres'])}")
530
- if filters['ratings']:
531
- st.write(f"**Ratings:** {', '.join(filters['ratings'])}")
532
- with filter_cols[1]:
533
- if filters['director']:
534
- st.write(f"**Director:** {filters['director']}")
535
- if filters['cast']:
536
- st.write(f"**Cast:** {filters['cast']}")
537
- st.write("---")
538
-
539
- # Pagination setup
540
- items_per_page = 10
541
- total_pages = (filtered_count + items_per_page - 1) // items_per_page
542
-
543
- # Initialize page number in session state if not exists
544
- if 'current_page' not in st.session_state:
545
- st.session_state.current_page = 1
546
-
547
- # Calculate slice indices for current page
548
- start_idx = (st.session_state.current_page - 1) * items_per_page
549
- end_idx = min(start_idx + items_per_page, filtered_count)
550
-
551
- # Display current range info
552
- st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
553
-
554
- # Show items for current page
555
- for i, content in enumerate(st.session_state.filtered_content[start_idx:end_idx], start=start_idx):
556
- with st.container():
557
- st.write(f"### {content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})")
558
-
559
- # Content details in columns
560
- detail_col1, detail_col2 = st.columns(2)
561
-
562
- with detail_col1:
563
- st.write(f"**πŸ“Ί Available on:** {content.get('streaming_service', 'N/A')}")
564
- st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
565
- st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
566
- st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
567
-
568
- with detail_col2:
569
- st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
570
- cast = content.get('cast')
571
- cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
572
- st.write(f"**πŸ‘₯ Cast:** {cast_display}")
573
- st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
574
-
575
- # Description
576
- st.write(f"**πŸ“– Description:**")
577
- st.write(content.get('description', 'N/A'))
578
-
579
- # Add Find Similar button with loading state
580
- similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{i}")
581
- if similar_button:
582
- with st.spinner("Finding similar content..."):
583
- similar_content = get_similar_content(content, n_recommendations=5)
584
-
585
- if similar_content:
586
- # Create tabs for different aspects of recommendations
587
- sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
588
-
589
- with sim_tab1:
590
- for sim_content in similar_content[:5]: # Show top 5 similar items
591
- with st.container():
592
- col1, col2 = st.columns([3, 1])
593
- with col1:
594
- st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
595
- st.write(f"*Available on:* {sim_content.get('streaming_service')}")
596
- st.write(f"*Genres:* {sim_content.get('listed_in')}")
597
- st.write(f"*Cast:* {sim_content.get('cast')}")
598
- st.write(f"*Director:* {sim_content.get('director')}")
599
- st.write(f"*Description:* {sim_content.get('description')}")
600
- with col2:
601
- st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
602
- st.write("---")
603
-
604
- with sim_tab2:
605
- st.write("**Why these recommendations?**")
606
- st.write("""
607
- These recommendations are based on multiple factors:
608
- - Genre and theme matching
609
- - Plot similarity analysis
610
- - Cast and director relationships
611
- - Release year proximity
612
-
613
- The percentage match indicates how similar each title is to your selection.
614
- """)
615
- else:
616
- st.info("No similar content found.")
617
- st.write("---")
618
 
619
- # Bottom pagination controls with better layout
620
- st.write("---")
621
- page_cols = st.columns([1, 2, 1, 2, 1])
622
 
623
- # Previous button
624
- with page_cols[0]:
625
- if st.button("← Previous", disabled=st.session_state.current_page == 1, use_container_width=True):
626
- st.session_state.current_page -= 1
627
- st.rerun()
628
 
629
- # Spacer
630
- with page_cols[1]:
631
- st.write("")
 
 
 
632
 
633
- # Page input
634
- with page_cols[2]:
635
- page_input = st.number_input(
636
- f"Page (of {total_pages})",
637
- min_value=1,
638
- max_value=total_pages,
639
- value=st.session_state.current_page,
640
- key="page_number",
641
- help=f"Enter a page number between 1 and {total_pages}"
642
- )
643
- if page_input != st.session_state.current_page:
644
- st.session_state.current_page = page_input
645
- st.rerun()
646
 
647
- # Spacer
648
- with page_cols[3]:
649
- st.write("")
 
 
 
 
 
 
 
 
650
 
651
- # Next button
652
- with page_cols[4]:
653
- if st.button("Next β†’", disabled=st.session_state.current_page == total_pages, use_container_width=True):
654
- st.session_state.current_page += 1
655
- st.rerun()
656
- else:
657
- st.info("No content matches your current filters. Try adjusting the criteria.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
- with col2:
660
- # AI Chat Section
661
- st.subheader("🎩 Your Personal Butler")
662
- st.write("How may I be of assistance in finding your perfect entertainment today?")
663
-
664
- # Model selection for Together AI
665
- model_choice = st.selectbox(
666
- "Select Your Butler's Expertise Level:",
667
- [
668
- "google/gemma-2b-it",
669
- "google/gemma-2-27b-it",
670
- "mistralai/Mistral-7B-Instruct-v0.1",
671
- "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
672
- "mistralai/Mixtral-8x7B-Instruct-v0.1"
673
- ],
674
- help="Select your butler's level of expertise in making recommendations"
675
- )
676
-
677
- # Example questions
678
- with st.expander("πŸ’‘ How to Address Your Butler"):
679
- st.write("""
680
- Your butler understands requests like:
681
- β€’ "My good sir, I seek an action film that would also please my companion who favors comedies."
682
- β€’ "Would you be so kind as to suggest a family-friendly show in the spirit of Stranger Things, but less frightening?"
683
- β€’ "I've quite enjoyed The Crown and Downton Abbey. Might you recommend similar period dramas?"
684
- β€’ "The weather is rather gloomy today. Perhaps a charming romantic comedy or musical?"
685
- β€’ "I'm in search of enlightening documentaries about technology or artificial intelligence."
686
- β€’ "We're hosting a gathering this evening. What entertainment would you suggest for a group?"
687
- """)
688
-
689
- user_question = st.text_area(
690
- "How May I Assist You?",
691
- placeholder="Tell me your preferences, and I shall curate the perfect selection...",
692
- height=100
693
  )
694
-
695
- if st.button("🎩 Request Recommendations", type="primary"):
696
- if user_question:
697
- with st.spinner("Your butler is carefully selecting the perfect entertainment..."):
698
- # Create context from current filtered data
699
- context = create_content_context(st.session_state.filtered_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
 
701
- try:
702
- # Get AI response
703
- ai_response = get_ai_response(client, user_question, context, model_choice)
704
-
705
- st.success("**🎩 Your Curated Selection:**")
706
- st.write(ai_response)
707
-
708
- # Show streaming availability
709
- with st.expander("🎩 Butler's Note"):
710
- st.write("""
711
- To access your selected entertainment:
712
- 1. Kindly select your preferred streaming services above
713
- 2. Locate your chosen title in the curated list
714
- 3. For similar recommendations, simply request "Find Similar Content"
715
-
716
- *Is there anything else I can assist you with?*
717
- """)
718
-
719
- except Exception as e:
720
- st.error("My sincerest apologies, but I seem to be unable to process your request at the moment. Might we try again?")
721
 
722
- else:
723
- st.warning("How may I be of assistance? Please share your entertainment preferences.")
724
-
725
- # Footer stats with butler theme
 
 
 
 
 
726
  st.markdown("---")
727
- if all_content:
728
- total_items = len(all_content)
729
- filtered_items = len(st.session_state.filtered_content)
730
-
731
- st.markdown("### 🎩 Your Entertainment Library")
732
-
733
- # Create columns for stats with better spacing
734
- stat_cols = st.columns(len(selected_services) + 3)
735
-
736
- # Basic stats with improved formatting
737
- with stat_cols[0]:
738
- st.metric("πŸ“š Complete Collection", f"{total_items:,}")
739
-
740
- with stat_cols[1]:
741
- st.metric("🎯 Curated Selection", f"{filtered_items:,}")
742
-
743
- with stat_cols[2]:
744
- movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
745
- shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
746
- st.metric("🎬 Films / πŸ“Ί Series", f"{movies:,} / {shows:,}")
747
-
748
- # Streaming service breakdown with icons
749
- service_icons = {
750
- "Netflix": "πŸ”΄",
751
- "Amazon Prime": "πŸ”΅",
752
- "Hulu": "🟒",
753
- "Disney+": "🟣"
754
- }
755
 
756
- for i, service in enumerate(selected_services, 3):
757
- if i < len(stat_cols):
758
- service_count = sum(1 for c in st.session_state.filtered_content if c.get('streaming_service') == service)
759
- icon = service_icons.get(service, "πŸ“Ί")
760
- with stat_cols[i]:
761
- st.metric(f"{icon} {service}", f"{service_count:,}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
 
763
  if __name__ == "__main__":
764
  main()
 
6
  import json
7
 
8
  # =============================================================================
9
+ # CONFIGURATION
10
  # =============================================================================
11
+ NOCODB_URL = "https://mtoft20-potm.hf.space".strip()
12
 
13
+ # =============================================================================
14
+ # API CLIENTS & CREDENTIALS
15
+ # =============================================================================
16
  def get_api_credentials():
17
+ """Get API credentials and endpoints from secrets or environment."""
18
+ def get_secret(key):
19
+ """Helper to get and clean secret values."""
20
+ return st.secrets.get(key, os.environ.get(key, "")).strip()
21
+
22
+ def get_endpoints(key):
23
+ """Helper to get and clean endpoint lists."""
24
+ return [ep.strip() for ep in get_secret(key).split(",") if ep.strip()]
25
+
26
+ return {
27
+ 'api_token': get_secret("NOCODB_API_TOKEN"),
28
+ 'together_key': get_secret("TOGETHER_API_KEY"),
29
+ 'content_endpoint': get_secret("NOCODB_CONTENT_ENDPOINT"),
30
+ 'similarity_endpoints': get_endpoints("NOCODB_SIMILARITY_ENDPOINTS")
31
+ }
 
 
 
 
 
 
 
 
 
32
 
 
33
  @st.cache_resource
34
  def get_ai_client():
35
+ """Initialize Together AI client."""
36
+ creds = get_api_credentials()
37
+ if not creds['together_key']:
38
  st.error("Together AI API key not found. Please configure it in the secrets.")
39
  return None
40
+ return Together(api_key=creds['together_key'])
41
 
42
  # =============================================================================
43
+ # DATABASE OPERATIONS
44
  # =============================================================================
45
+ def make_api_request(url, headers, params=None):
46
+ """Make API request with error handling."""
47
+ try:
48
+ response = requests.get(url, headers=headers, params=params)
49
+ response.raise_for_status()
50
+ return response.json()
51
+ except requests.exceptions.RequestException as e:
52
+ st.error(f"API request failed: {str(e)}")
53
+ return None
54
+
55
+ @st.cache_data(ttl=300)
56
  def get_streaming_content():
57
+ """Fetch streaming content from NocoDB with pagination."""
58
+ creds = get_api_credentials()
59
 
60
+ if not creds['api_token'] or not creds['content_endpoint']:
61
  st.error("NocoDB credentials not configured. Please set up your secrets.")
62
  return []
63
 
64
  headers = {
65
+ "xc-token": creds['api_token'],
66
  "accept": "application/json"
67
  }
68
 
69
  all_content = []
70
  page = 1
71
+ page_size = 1000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ while True:
74
+ url = f"{NOCODB_URL}{creds['content_endpoint']}?limit={page_size}&offset={(page-1)*page_size}"
75
+ data = make_api_request(url, headers)
 
 
76
 
77
+ if not data:
78
+ break
 
 
 
 
 
 
 
 
 
79
 
80
+ current_page = data.get('list', [])
81
+ if not current_page:
82
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ all_content.extend(current_page)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ if data.get('pageInfo', {}).get('isLastPage', True):
87
+ break
88
+
89
+ page += 1
90
 
91
+ return all_content
92
 
93
+ def get_similar_content(content, n_recommendations=5):
94
+ """Get pre-computed similar content from database."""
95
+ creds = get_api_credentials()
 
 
 
 
 
96
 
97
+ if not creds['api_token'] or not creds['similarity_endpoints']:
98
+ st.error("NocoDB credentials not configured properly.")
99
+ return []
100
 
101
+ headers = {
102
+ "xc-token": creds['api_token'],
103
+ "accept": "application/json"
104
+ }
 
105
 
106
+ # Build query parameters
107
+ query = {
108
+ "where": f'(show_id,eq,{content.get("show_id", "")})~and(title,eq,{content.get("title", "")})'
109
+ }
110
 
111
+ # Search through similarity endpoints
112
+ for endpoint in creds['similarity_endpoints']:
113
+ url = f"{NOCODB_URL}{endpoint}"
114
+ data = make_api_request(url, headers, query)
115
+
116
+ if not data or not data.get('list'):
117
+ continue
118
+
119
+ # Process similar items
120
+ for entry in data['list']:
121
+ try:
122
+ similar_items = json.loads(entry['similar_items'])
123
+ similar_content = []
124
+
125
+ # Get details for each similar item
126
+ for item in similar_items[:n_recommendations]:
127
+ content_query = {
128
+ "where": f'(show_id,eq,{item.get("show_id", "")})'
129
+ }
130
+ content_url = f"{NOCODB_URL}{creds['content_endpoint']}"
131
+ content_data = make_api_request(content_url, headers, content_query)
132
+
133
+ if content_data and content_data.get('list'):
134
+ content_dict = content_data['list'][0]
135
+ content_dict['similarity'] = f"{item['similarity']:.2%}"
136
+ similar_content.append(content_dict)
137
+
138
+ return similar_content[:n_recommendations]
139
+ except (json.JSONDecodeError, KeyError, IndexError):
140
+ continue
141
 
142
+ return []
143
 
144
+ # =============================================================================
145
+ # AI OPERATIONS
146
+ # =============================================================================
147
  def get_ai_response(client, question, context, model_name):
148
+ """Get AI response for user questions."""
149
  try:
150
+ prompt = f"""You are a sophisticated butler and streaming content expert. Based on the current content data,
151
+ please assist the user with their entertainment needs.
152
 
153
  Current Content Data Context:
154
  {context}
155
 
156
  User Question: {question}
157
 
158
+ Please provide a refined and helpful response, keeping it concise yet informative."""
159
 
160
  response = client.chat.completions.create(
161
  model=model_name,
162
  messages=[
163
+ {"role": "system", "content": "You are StreamButler, a sophisticated entertainment concierge with extensive knowledge of films and television."},
164
  {"role": "user", "content": prompt}
165
  ],
166
  max_tokens=300,
 
170
  return response.choices[0].message.content
171
 
172
  except Exception as e:
173
+ raise Exception(f"AI Service Error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ # =============================================================================
176
+ # DATA PROCESSING
177
+ # =============================================================================
178
+ def filter_content(content_list, filters):
179
+ """Apply filters to content list with error handling."""
180
  try:
181
+ return [
182
+ content for content in content_list
183
+ if all([
184
+ content.get('streaming_service') in filters['streaming_services'],
185
+ (not filters['content_type'] or content.get('type') == filters['content_type']),
186
+ (not filters['genres'] or all(g.strip().lower() in str(content.get('listed_in', '')).lower() for g in filters['genres'])),
187
+ (not filters['ratings'] or content.get('rating', '').strip() in filters['ratings']),
188
+ filters['year_range'][0] <= int(content.get('release_year', 0)) <= filters['year_range'][1],
189
+ (not filters['director'] or any(name.strip().lower() in str(content.get('director', '')).lower() for name in filters['director'].split(','))),
190
+ (not filters['cast'] or any(name.strip().lower() in str(content.get('cast', '')).lower() for name in filters['cast'].split(',')))
191
+ ])
192
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  except Exception as e:
194
+ st.error(f"Error filtering content: {str(e)}")
195
  return []
196
 
197
+ def create_content_context(content_list):
198
+ """Create context string about current content."""
199
+ if not content_list:
200
+ return "No content matches the current filters."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ total = len(content_list)
203
+ movies = sum(1 for c in content_list if c.get('type') == 'Movie')
204
+ shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
 
205
 
206
+ genres = pd.Series([
207
+ g.strip()
208
+ for c in content_list[:20]
209
+ for g in str(c.get('listed_in', '')).split(',')
210
+ ]).value_counts()
 
 
 
211
 
212
+ context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows). """
213
+ if not genres.empty:
214
+ context += f"Popular genres include: {', '.join(genres.head(5).index)}. "
215
 
216
+ return context
217
+
218
+ # =============================================================================
219
+ # UI COMPONENTS
220
+ # =============================================================================
221
+ def render_filters_sidebar(all_content):
222
+ """Render the filters sidebar."""
223
+ st.sidebar.header("πŸ” Filter Content")
224
 
225
+ # Extract filter options
226
  all_ratings = sorted(list(set(
227
  c.get('rating') for c in all_content
228
+ if c and isinstance(c.get('rating'), str)
229
+ and not c.get('rating').endswith('min')
230
+ and c.get('rating').strip()
 
 
231
  )))
232
+
233
  all_genres = sorted(list(set(
234
  genre.strip()
235
  for c in all_content
236
+ for genre in str(c.get('listed_in', '')).split(',')
237
  if genre.strip()
238
  )))
239
+
240
+ all_streaming_services = sorted(list(set(
241
+ c.get('streaming_service')
242
+ for c in all_content
243
+ if c.get('streaming_service')
244
+ )))
245
 
246
  # Extract unique directors and cast members
247
+ all_directors = sorted(list(set(
248
+ name.strip()
249
+ for c in all_content
250
+ for name in str(c.get('director', '')).split(',')
251
+ if name.strip()
252
+ )))
253
+
254
+ all_cast_members = sorted(list(set(
255
+ name.strip()
256
+ for c in all_content
257
+ for name in str(c.get('cast', '')).split(',')
258
+ if name.strip()
259
+ )))
260
 
 
 
 
261
  with st.sidebar.form("filter_form"):
262
+ # Streaming Services
263
  st.subheader("Streaming Services")
 
 
264
  selected_services = st.multiselect(
265
  "Select Your Streaming Services",
266
  options=all_streaming_services,
267
+ default=all_streaming_services[:1],
268
  help="Select the streaming services you have access to",
269
  key="streaming_services"
270
  )
 
272
  if not selected_services:
273
  st.warning("Please select at least one streaming service")
274
 
275
+ # Content Type
276
  st.subheader("Content Filters")
 
277
  content_type = st.selectbox(
278
  "Content Type",
279
  options=["All", "Movie", "TV Show"],
280
  index=0
281
  )
282
 
283
+ # Genres
284
  selected_genres = st.multiselect(
285
  "Genres",
286
  options=all_genres,
287
  default=[]
288
  )
 
 
289
 
290
+ # Optional Filters
291
+ st.subheader("Optional Filters")
292
  selected_ratings = st.multiselect(
293
  "Ratings",
294
  options=all_ratings,
295
  default=[],
296
  help="Filter by content rating"
297
  )
298
+
299
+ # Year range
300
  years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
301
  min_year, max_year = min(years), max(years)
302
  year_range = st.slider(
 
307
  help="Filter by release year range"
308
  )
309
 
310
+ # Duration range for movies
311
  movie_durations = [
312
  int(str(c.get('duration', '0 min')).split()[0])
313
  for c in all_content
314
  if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
315
  ]
316
 
317
+ duration_range = (0, 1000)
318
  if movie_durations:
319
  min_duration = min(d for d in movie_durations if d > 0)
320
  max_duration = max(movie_durations)
 
325
  value=(min_duration, max_duration),
326
  help="This filter only applies to movies"
327
  )
328
+
329
+ # Director and Cast filters
 
 
330
  selected_directors = st.multiselect(
331
  "Directors",
332
  options=all_directors,
 
334
  help="Select one or more directors (searchable)",
335
  placeholder="Start typing to search directors..."
336
  )
337
+
 
338
  selected_cast = st.multiselect(
339
  "Cast Members",
340
  options=all_cast_members,
 
343
  placeholder="Start typing to search cast members..."
344
  )
345
 
 
346
  apply_filters = st.form_submit_button("πŸ” Apply Filters", type="primary")
347
+
348
+ return {
349
+ 'streaming_services': selected_services,
350
+ 'content_type': content_type if content_type != "All" else None,
351
+ 'ratings': selected_ratings,
352
+ 'genres': selected_genres,
353
+ 'year_range': year_range,
354
+ 'duration_range': duration_range,
355
+ 'director': ','.join(selected_directors) if selected_directors else '',
356
+ 'cast': ','.join(selected_cast) if selected_cast else '',
357
+ 'apply_filters': apply_filters
358
+ }
359
+
360
+ def render_content_list(filtered_content, page, items_per_page=10):
361
+ """Render the content list with pagination."""
362
+ if not filtered_content:
363
+ st.subheader("πŸ“‹ No Titles Found")
364
+ return
365
 
366
+ filtered_count = len(filtered_content)
367
+ st.subheader(f"πŸ“‹ Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
 
 
 
 
 
 
 
 
 
368
 
369
+ # Calculate page info
370
+ total_pages = (filtered_count + items_per_page - 1) // items_per_page
371
+ start_idx = (page - 1) * items_per_page
372
+ end_idx = min(start_idx + items_per_page, filtered_count)
 
 
 
 
 
 
 
373
 
374
+ st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
375
+
376
+ # Display content items
377
+ for i, content in enumerate(filtered_content[start_idx:end_idx], start=start_idx):
378
+ with st.container():
379
+ st.write(f"### {content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
+ detail_col1, detail_col2 = st.columns(2)
 
 
382
 
383
+ with detail_col1:
384
+ st.write(f"**πŸ“Ί Available on:** {content.get('streaming_service', 'N/A')}")
385
+ st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
386
+ st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
387
+ st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
388
 
389
+ with detail_col2:
390
+ st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
391
+ cast = content.get('cast')
392
+ cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
393
+ st.write(f"**πŸ‘₯ Cast:** {cast_display}")
394
+ st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
395
 
396
+ st.write(f"**πŸ“– Description:**")
397
+ st.write(content.get('description', 'N/A'))
 
 
 
 
 
 
 
 
 
 
 
398
 
399
+ render_similar_content_section(content)
400
+ st.write("---")
401
+
402
+ render_pagination_controls(page, total_pages)
403
+
404
+ def render_similar_content_section(content):
405
+ """Render the similar content section for a content item."""
406
+ similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{content.get('show_id')}")
407
+ if similar_button:
408
+ with st.spinner("Finding similar content..."):
409
+ similar_content = get_similar_content(content, n_recommendations=5)
410
 
411
+ if similar_content:
412
+ sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
413
+
414
+ with sim_tab1:
415
+ for sim_content in similar_content:
416
+ with st.container():
417
+ col1, col2 = st.columns([3, 1])
418
+ with col1:
419
+ st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
420
+ st.write(f"*Available on:* {sim_content.get('streaming_service')}")
421
+ st.write(f"*Genres:* {sim_content.get('listed_in')}")
422
+ st.write(f"*Cast:* {sim_content.get('cast')}")
423
+ st.write(f"*Director:* {sim_content.get('director')}")
424
+ st.write(f"*Description:* {sim_content.get('description')}")
425
+ with col2:
426
+ st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
427
+ st.write("---")
428
+
429
+ with sim_tab2:
430
+ st.write("**Why these recommendations?**")
431
+ st.write("""
432
+ These recommendations are based on multiple factors:
433
+ - Genre and theme matching
434
+ - Plot similarity analysis
435
+ - Cast and director relationships
436
+ - Release year proximity
437
+
438
+ The percentage match indicates how similar each title is to your selection.
439
+ """)
440
+ else:
441
+ st.info("No similar content found.")
442
+
443
+ def render_pagination_controls(current_page, total_pages):
444
+ """Render pagination controls."""
445
+ st.write("---")
446
+ page_cols = st.columns([1, 2, 1, 2, 1])
447
 
448
+ with page_cols[0]:
449
+ if st.button("← Previous", disabled=current_page == 1, use_container_width=True):
450
+ st.session_state.current_page = max(1, current_page - 1)
451
+ st.rerun()
452
+
453
+ with page_cols[2]:
454
+ page_input = st.number_input(
455
+ f"Page (of {total_pages})",
456
+ min_value=1,
457
+ max_value=total_pages,
458
+ value=current_page,
459
+ key="page_number",
460
+ help=f"Enter a page number between 1 and {total_pages}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  )
462
+ if page_input != current_page:
463
+ st.session_state.current_page = page_input
464
+ st.rerun()
465
+
466
+ with page_cols[4]:
467
+ if st.button("Next β†’", disabled=current_page == total_pages, use_container_width=True):
468
+ st.session_state.current_page = min(total_pages, current_page + 1)
469
+ st.rerun()
470
+
471
+ def render_ai_chat_section(client, filtered_content):
472
+ """Render the AI chat section."""
473
+ st.subheader("🎩 Your Personal Butler")
474
+ st.write("How may I be of assistance in finding your perfect entertainment today?")
475
+
476
+ model_choice = st.selectbox(
477
+ "Select Your Butler's Expertise Level:",
478
+ [
479
+ "google/gemma-2b-it",
480
+ "google/gemma-2-27b-it",
481
+ "mistralai/Mistral-7B-Instruct-v0.1",
482
+ "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
483
+ "mistralai/Mixtral-8x7B-Instruct-v0.1"
484
+ ],
485
+ help="Select your butler's level of expertise in making recommendations"
486
+ )
487
+
488
+ with st.expander("πŸ’‘ How to Address Your Butler"):
489
+ st.write("""
490
+ Your butler understands requests like:
491
+ β€’ "My good sir, I seek an action film that would also please my companion who favors comedies."
492
+ β€’ "Would you be so kind as to suggest a family-friendly show in the spirit of Stranger Things, but less frightening?"
493
+ β€’ "I've quite enjoyed The Crown and Downton Abbey. Might you recommend similar period dramas?"
494
+ β€’ "The weather is rather gloomy today. Perhaps a charming romantic comedy or musical?"
495
+ β€’ "I'm in search of enlightening documentaries about technology or artificial intelligence."
496
+ β€’ "We're hosting a gathering this evening. What entertainment would you suggest for a group?"
497
+ """)
498
+
499
+ user_question = st.text_area(
500
+ "How May I Assist You?",
501
+ placeholder="Tell me your preferences, and I shall curate the perfect selection...",
502
+ height=100
503
+ )
504
+
505
+ if st.button("🎩 Request Recommendations", type="primary"):
506
+ if user_question:
507
+ with st.spinner("Your butler is carefully selecting the perfect entertainment..."):
508
+ context = create_content_context(filtered_content)
509
+ try:
510
+ ai_response = get_ai_response(client, user_question, context, model_choice)
511
+ st.success("**🎩 Your Curated Selection:**")
512
+ st.write(ai_response)
513
 
514
+ with st.expander("🎩 Butler's Note"):
515
+ st.write("""
516
+ To access your selected entertainment:
517
+ 1. Kindly select your preferred streaming services above
518
+ 2. Locate your chosen title in the curated list
519
+ 3. For similar recommendations, simply request "Find Similar Content"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
 
521
+ *Is there anything else I can assist you with?*
522
+ """)
523
+ except Exception as e:
524
+ st.error("My sincerest apologies, but I seem to be unable to process your request at the moment. Might we try again?")
525
+ else:
526
+ st.warning("How may I be of assistance? Please share your entertainment preferences.")
527
+
528
+ def render_footer_stats(all_content, filtered_content, selected_services):
529
+ """Render footer statistics."""
530
  st.markdown("---")
531
+ if not all_content:
532
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
+ total_items = len(all_content)
535
+ filtered_items = len(filtered_content)
536
+
537
+ st.markdown("### 🎩 Your Entertainment Library")
538
+
539
+ stat_cols = st.columns(len(selected_services) + 3)
540
+
541
+ with stat_cols[0]:
542
+ st.metric("πŸ“š Complete Collection", f"{total_items:,}")
543
+
544
+ with stat_cols[1]:
545
+ st.metric("🎯 Curated Selection", f"{filtered_items:,}")
546
+
547
+ with stat_cols[2]:
548
+ movies = sum(1 for c in filtered_content if c.get('type') == 'Movie')
549
+ shows = sum(1 for c in filtered_content if c.get('type') == 'TV Show')
550
+ st.metric("🎬 Films / πŸ“Ί Series", f"{movies:,} / {shows:,}")
551
+
552
+ service_icons = {
553
+ "Netflix": "πŸ”΄",
554
+ "Amazon Prime": "πŸ”΅",
555
+ "Hulu": "🟒",
556
+ "Disney+": "🟣"
557
+ }
558
+
559
+ for i, service in enumerate(selected_services, 3):
560
+ if i < len(stat_cols):
561
+ service_count = sum(1 for c in filtered_content if c.get('streaming_service') == service)
562
+ icon = service_icons.get(service, "πŸ“Ί")
563
+ with stat_cols[i]:
564
+ st.metric(f"{icon} {service}", f"{service_count:,}")
565
+
566
+ def main():
567
+ """Main application entry point."""
568
+ # Page config
569
+ st.set_page_config(
570
+ page_title="StreamButler - Your Personal Streaming Concierge",
571
+ page_icon="🎩",
572
+ layout="wide"
573
+ )
574
+
575
+ # Header
576
+ st.title("🎩 StreamButler")
577
+ st.write("*At your service! Allow me to curate the perfect streaming entertainment for you.*")
578
+
579
+ # Check API credentials
580
+ creds = get_api_credentials()
581
+ if not creds['together_key']:
582
+ st.error("⚠️ Together AI API key not configured!")
583
+ st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
584
+ st.stop()
585
+
586
+ if not creds['api_token'] or not creds['content_endpoint']:
587
+ st.error("⚠️ NocoDB credentials not configured!")
588
+ st.info("Please set NOCODB_API_TOKEN and NOCODB_CONTENT_ENDPOINT in the Hugging Face Spaces secrets.")
589
+ st.stop()
590
+
591
+ # Initialize AI client
592
+ try:
593
+ client = get_ai_client()
594
+ if not client:
595
+ st.stop()
596
+ except Exception as e:
597
+ st.error(f"Failed to initialize Together AI client: {e}")
598
+ st.stop()
599
+
600
+ # Load content
601
+ with st.spinner("Loading streaming content..."):
602
+ all_content = get_streaming_content()
603
+
604
+ if not all_content:
605
+ st.error("Could not load streaming content. Please check your NocoDB connection.")
606
+ st.stop()
607
+
608
+ # Render filters sidebar
609
+ filters = render_filters_sidebar(all_content)
610
+
611
+ # Apply filters
612
+ if filters['apply_filters']:
613
+ filtered_content = filter_content(all_content, filters)
614
+ st.session_state.filtered_content = filtered_content
615
+ else:
616
+ if 'filtered_content' not in st.session_state:
617
+ st.session_state.filtered_content = all_content
618
+
619
+ # Initialize page number
620
+ if 'current_page' not in st.session_state:
621
+ st.session_state.current_page = 1
622
+
623
+ # Main content layout
624
+ col1, col2 = st.columns([2, 1])
625
+
626
+ with col1:
627
+ render_content_list(st.session_state.filtered_content, st.session_state.current_page)
628
+
629
+ with col2:
630
+ render_ai_chat_section(client, st.session_state.filtered_content)
631
+
632
+ # Footer stats
633
+ render_footer_stats(all_content, st.session_state.filtered_content, filters['streaming_services'])
634
 
635
  if __name__ == "__main__":
636
  main()