mtoft20 commited on
Commit
8900764
·
verified ·
1 Parent(s): 2801836

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +167 -853
src/streamlit_app.py CHANGED
@@ -1,853 +1,167 @@
1
- import streamlit as st
2
- import requests
3
- import pandas as pd
4
- from together import Together
5
- import os
6
- import json
7
- from collections import defaultdict
8
- import re
9
-
10
- # =============================================================================
11
- # CONFIGURATION - Using Secrets Management
12
- # =============================================================================
13
- NOCODB_URL = "https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/mvtt3arw5ni7uqp" # Updated with table ID
14
-
15
- # Get sensitive data from Streamlit secrets or environment variables
16
- def get_api_credentials():
17
- """Get API credentials from secrets or environment"""
18
- try:
19
- # Try Streamlit secrets first (for Hugging Face Spaces)
20
- api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", ""))
21
- together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", ""))
22
-
23
- return api_token, together_key
24
- except:
25
- # Fallback to environment variables
26
- api_token = os.environ.get("NOCODB_API_TOKEN", "")
27
- together_key = os.environ.get("TOGETHER_API_KEY", "")
28
-
29
- return api_token, together_key
30
-
31
- # Initialize Together AI client
32
- @st.cache_resource
33
- def get_ai_client():
34
- """Initialize Together AI client"""
35
- _, together_key = get_api_credentials()
36
- if not together_key:
37
- st.error("Together AI API key not found. Please configure it in the secrets.")
38
- return None
39
- return Together(api_key=together_key)
40
-
41
- # =============================================================================
42
- # HELPER FUNCTIONS
43
- # =============================================================================
44
- @st.cache_data(ttl=300) # Cache for 5 minutes
45
- def get_streaming_content():
46
- """Fetch streaming content from NocoDB with pagination"""
47
- api_token, _ = get_api_credentials()
48
-
49
- if not api_token:
50
- st.error("NocoDB credentials not configured. Please set up your secrets.")
51
- return []
52
-
53
- headers = {
54
- "xc-token": api_token,
55
- "accept": "application/json"
56
- }
57
-
58
- all_content = []
59
- page = 1
60
- page_size = 1000 # NocoDB default page size
61
-
62
- try:
63
- while True:
64
- offset = (page - 1) * page_size
65
- url = f"{NOCODB_URL}?limit={page_size}&offset={offset}"
66
-
67
- response = requests.get(url, headers=headers)
68
-
69
- if response.status_code == 200:
70
- data = response.json()
71
- current_page_data = data.get('list', [])
72
-
73
- # Filter out None values and ensure all items are dictionaries
74
- current_page_data = [item for item in current_page_data if item and isinstance(item, dict)]
75
-
76
- if not current_page_data: # No more data to fetch
77
- break
78
-
79
- all_content.extend(current_page_data)
80
-
81
- # Check if this is the last page
82
- page_info = data.get('pageInfo', {})
83
- if page_info.get('isLastPage', True):
84
- break
85
-
86
- page += 1
87
- else:
88
- st.error(f"Failed to fetch data: {response.status_code}")
89
- if not all_content: # Only return [] if we haven't fetched any data
90
- return []
91
- break # If we have some data, return what we've got
92
-
93
- return all_content
94
-
95
- except Exception as e:
96
- st.error(f"Error connecting to database: {str(e)}")
97
- st.write("Full error details:", e)
98
- return []
99
-
100
- def filter_content(content_list, filters):
101
- """Apply filters to streaming content list"""
102
- filtered = []
103
-
104
- for content in content_list:
105
- if not content or not isinstance(content, dict):
106
- continue
107
-
108
- matches_all_filters = True
109
-
110
- # Streaming service filter
111
- if filters['streaming_services']:
112
- if content.get('streaming_service') not in filters['streaming_services']:
113
- matches_all_filters = False
114
- continue
115
-
116
- # Type filter - only apply if not "All"
117
- if filters['content_type']:
118
- if content.get('type') != filters['content_type']:
119
- matches_all_filters = False
120
- continue
121
-
122
- # Genre filter - check if ALL selected genres are in the content's genres
123
- if filters['genres']:
124
- content_genres = set(g.strip().lower() for g in str(content.get('listed_in', '')).split(','))
125
- selected_genres = set(g.strip().lower() for g in filters['genres'])
126
-
127
- if not selected_genres.issubset(content_genres):
128
- matches_all_filters = False
129
- continue
130
-
131
- # Rating filter
132
- if filters['ratings']:
133
- rating = content.get('rating', '').strip()
134
- # Only compare if rating is a valid string and not a duration
135
- if not rating or not isinstance(rating, str) or rating.endswith('min'):
136
- matches_all_filters = False
137
- continue
138
- if rating not in filters['ratings']:
139
- matches_all_filters = False
140
- continue
141
-
142
- # Release year filter
143
- try:
144
- release_year = int(content.get('release_year', 0))
145
- if release_year < filters['year_range'][0] or release_year > filters['year_range'][1]:
146
- matches_all_filters = False
147
- continue
148
- except (ValueError, TypeError):
149
- matches_all_filters = False
150
- continue
151
-
152
- # Duration filter (different handling for movies)
153
- if filters['content_type'] == 'Movie':
154
- duration = str(content.get('duration', ''))
155
- if 'min' in duration:
156
- try:
157
- minutes = int(duration.split()[0])
158
- if minutes < filters['duration_range'][0] or minutes > filters['duration_range'][1]:
159
- matches_all_filters = False
160
- continue
161
- except (ValueError, IndexError):
162
- matches_all_filters = False
163
- continue
164
-
165
- # Director filter (optional)
166
- if filters['director']:
167
- director = str(content.get('director', '')).lower()
168
- if not any(name.strip().lower() in director for name in filters['director'].split(',')):
169
- matches_all_filters = False
170
- continue
171
-
172
- # Cast filter (optional)
173
- if filters['cast']:
174
- cast = str(content.get('cast', '')).lower()
175
- if not any(name.strip().lower() in cast for name in filters['cast'].split(',')):
176
- matches_all_filters = False
177
- continue
178
-
179
- if matches_all_filters:
180
- filtered.append(content)
181
-
182
- return filtered
183
-
184
- def create_content_context(content_list):
185
- """Create context string about current content for AI"""
186
- if not content_list:
187
- return "No content matches the current filters."
188
-
189
- total = len(content_list)
190
- movies = sum(1 for c in content_list if c.get('type') == 'Movie')
191
- shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
192
-
193
- context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows). """
194
-
195
- # Add some genre info
196
- all_genres = []
197
- for content in content_list[:20]: # Sample from first 20 items
198
- genres = content.get('listed_in', '').split(', ')
199
- all_genres.extend(genres)
200
-
201
- if all_genres:
202
- genre_counts = pd.Series(all_genres).value_counts()
203
- top_genres = genre_counts.head(5).index.tolist()
204
- context += f"Top genres include: {', '.join(top_genres)}. "
205
-
206
- # Add year range
207
- years = [int(c.get('release_year', 0)) for c in content_list if c.get('release_year')]
208
- if years:
209
- context += f"Release years range from {min(years)} to {max(years)}."
210
-
211
- return context
212
-
213
- def get_ai_response(client, question, context, model_name):
214
- """Get response from Together AI"""
215
- try:
216
- prompt = f"""You are a helpful streaming content expert. Based on the current content data, please answer the user's question accurately and helpfully.
217
-
218
- Current Content Data Context:
219
- {context}
220
-
221
- User Question: {question}
222
-
223
- Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative."""
224
-
225
- response = client.chat.completions.create(
226
- model=model_name,
227
- messages=[
228
- {"role": "system", "content": "You are a helpful content expert with deep knowledge of movies and TV shows."},
229
- {"role": "user", "content": prompt}
230
- ],
231
- max_tokens=300,
232
- temperature=0.7,
233
- )
234
-
235
- return response.choices[0].message.content
236
-
237
- except Exception as e:
238
- raise Exception(f"Together AI Error: {str(e)}")
239
-
240
- def extract_unique_names(content_list, field):
241
- """Extract unique names from a comma-separated field in content list"""
242
- unique_names = set()
243
- for content in content_list:
244
- names = content.get(field, '')
245
- if names:
246
- # Split by comma and clean each name
247
- for name in names.split(','):
248
- cleaned_name = name.strip()
249
- if cleaned_name: # Only add non-empty names
250
- unique_names.add(cleaned_name)
251
- return sorted(list(unique_names))
252
-
253
- def get_similar_content(content, n_recommendations=5):
254
- """Get pre-computed similar content from database"""
255
- try:
256
- # Get database credentials
257
- api_token, _ = get_api_credentials()
258
- headers = {
259
- "xc-token": api_token,
260
- "accept": "application/json"
261
- }
262
-
263
- # Debug: Print the title and show_id we're searching for
264
- title = content.get('title', '')
265
- show_id = content.get('show_id', '')
266
- st.write(f"🔍 Searching for similar content to: {title} (ID: {show_id})")
267
-
268
- # Base URL for the NocoDB API
269
- base_url = "https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee"
270
-
271
- # URLs for both similarity tables using table IDs
272
- similarity_table_urls = [
273
- f"{base_url}/mp7bnn9tzhojh7k", # Part 1 similarities
274
- f"{base_url}/m8e5rglns4acmef", # Part 2 similarities
275
- f"{base_url}/m2driodimid10k6" # Part 3 similarities
276
- ]
277
-
278
- similar_items = []
279
-
280
- # Check both tables for similarities
281
- for table_url in similarity_table_urls:
282
- # Debug: Get a sample row to see table structure
283
- sample_params = {
284
- "limit": 1
285
- }
286
- sample_response = requests.get(table_url, headers=headers, params=sample_params)
287
- if sample_response.status_code == 200:
288
- sample_data = sample_response.json()
289
- if sample_data.get('list'):
290
- st.write(f"📋 Table structure for {table_url.split('/')[-1]}:")
291
- st.write("Columns:", list(sample_data['list'][0].keys()))
292
- st.write("Sample row:", sample_data['list'][0])
293
-
294
- # Try finding by show_id
295
- query = f'(show_id,eq,"{show_id}")'
296
- alt_query = f'(show_id,like,"{show_id}")' # Try alternative query format
297
-
298
- params = {
299
- "where": query
300
- }
301
- alt_params = {
302
- "where": alt_query
303
- }
304
-
305
- # Debug: Print the request details
306
- st.write(f"📡 Querying table: {table_url.split('/')[-1]}")
307
- st.write(f"🔍 Query params: {params}")
308
-
309
- # Try first query format
310
- try:
311
- response = requests.get(table_url, headers=headers, params=params)
312
- st.write(f"📥 Response status: {response.status_code}")
313
- st.write("📥 Request URL:", response.url)
314
-
315
- # If first query returns no results, try alternative query
316
- if response.status_code == 200:
317
- data = response.json()
318
- if not data.get('list'):
319
- st.write("🔄 Trying alternative query format...")
320
- response = requests.get(table_url, headers=headers, params=alt_params)
321
- st.write(f"📥 Alt response status: {response.status_code}")
322
- st.write("📥 Alt request URL:", response.url)
323
- if response.status_code == 200:
324
- data = response.json()
325
-
326
- # Debug: Print raw response
327
- try:
328
- response_json = response.json()
329
- st.write("Raw response:", response_json)
330
-
331
- # Additional debugging
332
- if response_json.get('list'):
333
- st.write("✅ Found entries in response")
334
- for entry in response_json['list']:
335
- st.write(f"Entry show_id: {entry.get('show_id')}")
336
- else:
337
- st.write("❌ No entries found in response")
338
-
339
- # Try to get a few random entries to verify data
340
- sample_params = {
341
- "limit": 3,
342
- "shuffle": true
343
- }
344
- sample_response = requests.get(table_url, headers=headers, params=sample_params)
345
- if sample_response.status_code == 200:
346
- sample_data = sample_response.json()
347
- st.write("🔍 Random entries from table:")
348
- for entry in sample_data.get('list', []):
349
- st.write(f"- {entry.get('title')} (ID: {entry.get('show_id')})")
350
-
351
- except Exception as e:
352
- st.write("Could not parse response as JSON:", response.text)
353
-
354
- if response.status_code == 200:
355
- data = response_json
356
- st.write(f"📊 Found {len(data.get('list', []))} matches in table")
357
-
358
- if data and len(data.get('list', [])) > 0:
359
- # Get similar items from stored data
360
- try:
361
- table_items = json.loads(data['list'][0]['similar_items'])
362
- st.write(f"✅ Successfully parsed {len(table_items)} similar items")
363
- similar_items.extend(table_items)
364
- except Exception as parse_error:
365
- st.write(f"❌ Error parsing similar items: {str(parse_error)}")
366
- st.write("Raw data:", data['list'][0])
367
- except Exception as e:
368
- st.write(f"❌ Request error: {str(e)}")
369
-
370
- st.write(f"📝 Total similar items found: {len(similar_items)}")
371
-
372
- if similar_items:
373
- # Sort by similarity score and limit to requested number
374
- similar_items.sort(key=lambda x: x['similarity'], reverse=True)
375
- similar_items = similar_items[:n_recommendations]
376
-
377
- # Get full content details for each similar item
378
- similar_content = []
379
- for item in similar_items:
380
- # Query main content table for full details using show_id if available
381
- show_id = item.get('show_id', '')
382
- if show_id:
383
- query = f'(show_id,eq,"{show_id}")'
384
- else:
385
- # Fallback to title if show_id not available
386
- query = f'(title,eq,"{item["title"]}")'
387
-
388
- content_params = {
389
- "where": query
390
- }
391
- content_response = requests.get(NOCODB_URL, headers=headers, params=content_params)
392
-
393
- if content_response.status_code == 200:
394
- content_data = content_response.json()
395
- if content_data and len(content_data.get('list', [])) > 0:
396
- content_dict = content_data['list'][0]
397
- content_dict['similarity'] = f"{item['similarity']:.2%}"
398
- similar_content.append(content_dict)
399
- st.write(f"✅ Found details for: {item['title']}")
400
- else:
401
- st.write(f"❌ No content details found for: {item['title']}")
402
-
403
- return similar_content
404
-
405
- return []
406
- except Exception as e:
407
- st.error(f"Error fetching similar content: {str(e)}")
408
- st.write("Full error details:", e)
409
- return []
410
-
411
- # =============================================================================
412
- # MAIN APP
413
- # =============================================================================
414
- def main():
415
- # Page config
416
- st.set_page_config(
417
- page_title="Streaming Content Explorer",
418
- page_icon="🎬",
419
- layout="wide"
420
- )
421
-
422
- # Header
423
- st.title("🎬 Streaming Content Explorer")
424
- st.write("Explore movies and TV shows across multiple streaming platforms!")
425
-
426
- # Check API credentials
427
- api_token, together_key = get_api_credentials()
428
-
429
- if not together_key:
430
- st.error("⚠️ Together AI API key not configured!")
431
- st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
432
- st.stop()
433
-
434
- if not api_token:
435
- st.error("⚠️ NocoDB credentials not configured!")
436
- st.info("Please set NOCODB_API_TOKEN in the Hugging Face Spaces secrets.")
437
- st.stop()
438
-
439
- # Initialize AI client
440
- try:
441
- client = get_ai_client()
442
- if not client:
443
- st.stop()
444
- except Exception as e:
445
- st.error(f"Failed to initialize Together AI client: {e}")
446
- st.stop()
447
-
448
- # Load all content first
449
- with st.spinner("Loading streaming content..."):
450
- all_content = get_streaming_content()
451
-
452
- if not all_content:
453
- st.error("Could not load streaming content. Please check your NocoDB connection.")
454
- st.stop()
455
-
456
- # Extract unique values for filters
457
- all_ratings = sorted(list(set(
458
- c.get('rating') for c in all_content
459
- if c and isinstance(c, dict)
460
- and c.get('rating')
461
- and isinstance(c.get('rating'), str)
462
- and not c.get('rating').endswith('min') # Exclude duration values
463
- and c.get('rating').strip() # Exclude empty strings
464
- )))
465
- all_genres = sorted(list(set(
466
- genre.strip()
467
- for c in all_content
468
- for genre in c.get('listed_in', '').split(',')
469
- if genre.strip()
470
- )))
471
- all_streaming_services = sorted(list(set([c.get('streaming_service') for c in all_content if c.get('streaming_service')])))
472
-
473
- # Extract unique directors and cast members
474
- all_directors = extract_unique_names(all_content, 'director')
475
- all_cast_members = extract_unique_names(all_content, 'cast')
476
-
477
- # Sidebar filters
478
- st.sidebar.header("🔍 Filter Content")
479
-
480
- with st.sidebar.form("filter_form"):
481
- st.subheader("Streaming Services")
482
-
483
- # Streaming service selection (required)
484
- selected_services = st.multiselect(
485
- "Select Your Streaming Services",
486
- options=all_streaming_services,
487
- default=all_streaming_services[:1], # Default to first service
488
- help="Select the streaming services you have access to",
489
- key="streaming_services"
490
- )
491
-
492
- if not selected_services:
493
- st.warning("Please select at least one streaming service")
494
-
495
- st.subheader("Content Filters")
496
-
497
- content_type = st.selectbox(
498
- "Content Type",
499
- options=["All", "Movie", "TV Show"],
500
- index=0
501
- )
502
-
503
- selected_genres = st.multiselect(
504
- "Genres",
505
- options=all_genres,
506
- default=[]
507
- )
508
-
509
- st.subheader("Optional Filters")
510
-
511
- # Rating filter
512
- selected_ratings = st.multiselect(
513
- "Ratings",
514
- options=all_ratings,
515
- default=[],
516
- help="Filter by content rating"
517
- )
518
-
519
- # Year range slider
520
- years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
521
- min_year, max_year = min(years), max(years)
522
- year_range = st.slider(
523
- "Release Year",
524
- min_value=min_year,
525
- max_value=max_year,
526
- value=(min_year, max_year),
527
- help="Filter by release year range"
528
- )
529
-
530
- # Duration range slider (for movies only)
531
- movie_durations = [
532
- int(str(c.get('duration', '0 min')).split()[0])
533
- for c in all_content
534
- if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
535
- ]
536
-
537
- if movie_durations:
538
- min_duration = min(d for d in movie_durations if d > 0)
539
- max_duration = max(movie_durations)
540
- duration_range = st.slider(
541
- "Movie Duration (minutes)",
542
- min_value=min_duration,
543
- max_value=max_duration,
544
- value=(min_duration, max_duration),
545
- help="This filter only applies to movies"
546
- )
547
- else:
548
- duration_range = (0, 1000) # Fallback values
549
-
550
- # Director filter with autocomplete
551
- selected_directors = st.multiselect(
552
- "Directors",
553
- options=all_directors,
554
- default=[],
555
- help="Select one or more directors (searchable)",
556
- placeholder="Start typing to search directors..."
557
- )
558
-
559
- # Cast filter with autocomplete
560
- selected_cast = st.multiselect(
561
- "Cast Members",
562
- options=all_cast_members,
563
- default=[],
564
- help="Select one or more cast members (searchable)",
565
- placeholder="Start typing to search cast members..."
566
- )
567
-
568
- # Submit button
569
- apply_filters = st.form_submit_button("🔍 Apply Filters", type="primary")
570
-
571
- # Create filter dictionary
572
- filters = {
573
- 'streaming_services': selected_services,
574
- 'content_type': content_type if content_type != "All" else None,
575
- 'ratings': selected_ratings,
576
- 'genres': selected_genres,
577
- 'year_range': year_range,
578
- 'duration_range': duration_range,
579
- 'director': ','.join(selected_directors) if selected_directors else '',
580
- 'cast': ','.join(selected_cast) if selected_cast else ''
581
- }
582
-
583
- # Only apply filters when the button is clicked
584
- if apply_filters:
585
- filtered_content = filter_content(all_content, filters)
586
- st.session_state.filtered_content = filtered_content
587
- else:
588
- # Initialize filtered content if not exists
589
- if 'filtered_content' not in st.session_state:
590
- st.session_state.filtered_content = all_content
591
-
592
- # Main content area
593
- col1, col2 = st.columns([2, 1])
594
-
595
- with col1:
596
- # Content listings
597
- filtered_count = len(st.session_state.filtered_content)
598
- if filtered_count == 0:
599
- st.subheader("📋 No Titles Found")
600
- else:
601
- # Header with count and page info
602
- st.subheader(f"📋 Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
603
-
604
- if st.session_state.filtered_content:
605
- # Active Filters section with better formatting
606
- if any([filters['content_type'], filters['genres'], filters['ratings'],
607
- filters['director'], filters['cast']]):
608
- with st.expander("🔍 Active Filters", expanded=True):
609
- filter_cols = st.columns(2)
610
- with filter_cols[0]:
611
- if filters['content_type']:
612
- st.write(f"**Type:** {filters['content_type']}")
613
- if filters['genres']:
614
- st.write(f"**Genres:** {', '.join(filters['genres'])}")
615
- if filters['ratings']:
616
- st.write(f"**Ratings:** {', '.join(filters['ratings'])}")
617
- with filter_cols[1]:
618
- if filters['director']:
619
- st.write(f"**Director:** {filters['director']}")
620
- if filters['cast']:
621
- st.write(f"**Cast:** {filters['cast']}")
622
- st.write("---")
623
-
624
- # Pagination setup
625
- items_per_page = 10
626
- total_pages = (filtered_count + items_per_page - 1) // items_per_page
627
-
628
- # Initialize page number in session state if not exists
629
- if 'current_page' not in st.session_state:
630
- st.session_state.current_page = 1
631
-
632
- # Calculate slice indices for current page
633
- start_idx = (st.session_state.current_page - 1) * items_per_page
634
- end_idx = min(start_idx + items_per_page, filtered_count)
635
-
636
- # Display current range info
637
- st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
638
-
639
- # Show items for current page
640
- for i, content in enumerate(st.session_state.filtered_content[start_idx:end_idx], start=start_idx):
641
- with st.expander(f"{content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})"):
642
- # Content details in columns
643
- detail_col1, detail_col2 = st.columns(2)
644
-
645
- with detail_col1:
646
- st.write(f"**📺 Available on:** {content.get('streaming_service', 'N/A')}")
647
- st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
648
- st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
649
- st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
650
-
651
- with detail_col2:
652
- st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
653
- cast = content.get('cast')
654
- cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
655
- st.write(f"**👥 Cast:** {cast_display}")
656
- st.write(f"**📝 Director:** {content.get('director', 'N/A')}")
657
-
658
- # Description
659
- st.write(f"**📖 Description:**")
660
- st.write(content.get('description', 'N/A'))
661
-
662
- # Add Find Similar button with loading state
663
- similar_button = st.button(f"🔍 Find Similar Content", key=f"similar_{i}")
664
- if similar_button:
665
- with st.spinner("Finding similar content..."):
666
- similar_content = get_similar_content(content)
667
-
668
- if similar_content:
669
- # Create tabs for different aspects of recommendations
670
- sim_tab1, sim_tab2 = st.tabs(["📺 Similar Titles", "🔍 Why These Recommendations"])
671
-
672
- with sim_tab1:
673
- for sim_content in similar_content:
674
- with st.container():
675
- col1, col2 = st.columns([3, 1])
676
- with col1:
677
- st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
678
- st.write(f"*Available on:* {sim_content.get('streaming_service')}")
679
- st.write(f"*Genres:* {sim_content.get('listed_in')}")
680
- with col2:
681
- st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
682
-
683
- with st.expander("See more details"):
684
- st.write(f"**Cast:** {sim_content.get('cast', 'N/A')}")
685
- st.write(f"**Director:** {sim_content.get('director', 'N/A')}")
686
- st.write(f"**Description:** {sim_content.get('description', 'N/A')}")
687
- st.write("---")
688
-
689
- with sim_tab2:
690
- st.write("**Why these recommendations?**")
691
- st.write("""
692
- These recommendations are pre-computed using advanced content analysis:
693
- - Genre and theme matching
694
- - Plot similarity analysis
695
- - Cast and director relationships
696
- - Release year proximity
697
-
698
- The percentage match indicates how similar each title is to your selection.
699
- """)
700
- else:
701
- st.info("No similar content found.")
702
-
703
- # Bottom pagination controls with better layout
704
- st.write("---")
705
- page_cols = st.columns([1, 2, 1, 2, 1])
706
-
707
- # Previous button
708
- with page_cols[0]:
709
- if st.button("← Previous", disabled=st.session_state.current_page == 1, use_container_width=True):
710
- st.session_state.current_page -= 1
711
- st.rerun()
712
-
713
- # Spacer
714
- with page_cols[1]:
715
- st.write("")
716
-
717
- # Page input
718
- with page_cols[2]:
719
- page_input = st.number_input(
720
- f"Page (of {total_pages})",
721
- min_value=1,
722
- max_value=total_pages,
723
- value=st.session_state.current_page,
724
- key="page_number",
725
- help=f"Enter a page number between 1 and {total_pages}"
726
- )
727
- if page_input != st.session_state.current_page:
728
- st.session_state.current_page = page_input
729
- st.rerun()
730
-
731
- # Spacer
732
- with page_cols[3]:
733
- st.write("")
734
-
735
- # Next button
736
- with page_cols[4]:
737
- if st.button("Next →", disabled=st.session_state.current_page == total_pages, use_container_width=True):
738
- st.session_state.current_page += 1
739
- st.rerun()
740
- else:
741
- st.info("No content matches your current filters. Try adjusting the criteria.")
742
-
743
- with col2:
744
- # AI Chat Section
745
- st.subheader("🤖 Ask AI Assistant")
746
- st.write("Ask questions about the streaming content!")
747
-
748
- # Model selection for Together AI
749
- model_choice = st.selectbox(
750
- "Select AI Model:",
751
- [
752
- "google/gemma-2b-it",
753
- "google/gemma-2-27b-it",
754
- "mistralai/Mistral-7B-Instruct-v0.1",
755
- "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
756
- "mistralai/Mixtral-8x7B-Instruct-v0.1"
757
- ],
758
- help="Select an AI model to answer your questions"
759
- )
760
-
761
- # Example questions
762
- with st.expander("💡 Example Questions"):
763
- st.write("• What are the most common genres?")
764
- st.write("• What's the average movie duration?")
765
- st.write("• Which directors have the most content?")
766
- st.write("• What are the trends in content ratings?")
767
- st.write("• Tell me about the release year distribution")
768
-
769
- user_question = st.text_area(
770
- "Your Question:",
771
- placeholder="Ask about genres, ratings, directors, trends...",
772
- height=100
773
- )
774
-
775
- if st.button("🔍 Ask AI", type="primary"):
776
- if user_question:
777
- with st.spinner("AI is analyzing the content..."):
778
- # Create context from current filtered data
779
- context = create_content_context(st.session_state.filtered_content)
780
-
781
- try:
782
- # Get AI response
783
- ai_response = get_ai_response(client, user_question, context, model_choice)
784
-
785
- st.success("**AI Assistant Response:**")
786
- st.write(ai_response)
787
-
788
- # Show debug info
789
- with st.expander("Debug Info"):
790
- st.write(f"Model used: {model_choice}")
791
- st.write(f"Content items analyzed: {len(st.session_state.filtered_content)}")
792
- st.write(f"Context: {context[:150]}...")
793
-
794
- except Exception as e:
795
- st.error(f"AI Error: {str(e)}")
796
-
797
- # Fallback response with data analysis
798
- st.info("**Fallback Analysis:**")
799
- if st.session_state.filtered_content:
800
- movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
801
- shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
802
- st.write(f"• Found {len(st.session_state.filtered_content)} titles ({movies} movies, {shows} TV shows)")
803
-
804
- # Show top genres
805
- genres = [g.strip() for c in st.session_state.filtered_content for g in c.get('listed_in', '').split(',')]
806
- genre_counts = pd.Series(genres).value_counts()
807
- st.write(f"• Top genres: {', '.join(genre_counts.head(3).index)}")
808
-
809
- # Show year range
810
- years = [int(c.get('release_year', 0)) for c in st.session_state.filtered_content if c.get('release_year')]
811
- if years:
812
- st.write(f"• Release years: {min(years)} - {max(years)}")
813
- else:
814
- st.warning("Please enter a question first!")
815
-
816
- # Footer stats with improved layout
817
- st.markdown("---")
818
- if all_content:
819
- total_items = len(all_content)
820
- filtered_items = len(st.session_state.filtered_content)
821
-
822
- # Create columns for stats with better spacing
823
- stat_cols = st.columns(len(selected_services) + 3)
824
-
825
- # Basic stats with improved formatting
826
- with stat_cols[0]:
827
- st.metric("📊 Total Available", f"{total_items:,}")
828
-
829
- with stat_cols[1]:
830
- st.metric("🔍 Filtered Results", f"{filtered_items:,}")
831
-
832
- with stat_cols[2]:
833
- movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
834
- shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
835
- st.metric("🎬 Movies / 📺 Shows", f"{movies:,} / {shows:,}")
836
-
837
- # Streaming service breakdown with icons
838
- service_icons = {
839
- "Netflix": "🔴",
840
- "Amazon Prime": "🔵",
841
- "Hulu": "🟢",
842
- "Disney+": "🟣"
843
- }
844
-
845
- for i, service in enumerate(selected_services, 3):
846
- if i < len(stat_cols):
847
- service_count = sum(1 for c in st.session_state.filtered_content if c.get('streaming_service') == service)
848
- icon = service_icons.get(service, "📺")
849
- with stat_cols[i]:
850
- st.metric(f"{icon} {service}", f"{service_count:,}")
851
-
852
- if __name__ == "__main__":
853
- main()
 
1
+ 🔍 Searching for similar content to: Dick Johnson Is Dead (ID: s1)
2
+
3
+ 📋 Table structure for mp7bnn9tzhojh7k:
4
+
5
+ Columns:
6
+
7
+ [
8
+ 0:"Id"
9
+ 1:"CreatedAt"
10
+ 2:"UpdatedAt"
11
+ 3:"title"
12
+ 4:"show_id"
13
+ 5:"similar_items"
14
+ ]
15
+ Sample row:
16
+
17
+ {
18
+ "Id":1
19
+ "CreatedAt":"2025-06-02 09:34:10+00:00"
20
+ "UpdatedAt":NULL
21
+ "title":"Dick Johnson Is Dead"
22
+ "show_id":"s1"
23
+ "similar_items":"[{"title": "Triple Threat", "show_id": "s3718", "similarity": 0.7407444080262803}, {"title": "The Death and Life of Marsha P. Johnson", "show_id": "s5234", "similarity": 0.7343378197080184}, {"title": "Toxic Beauty", "show_id": "s1715", "similarity": 0.7338750075490208}, {"title": "The Possession of Michael King", "show_id": "s8177", "similarity": 0.731497980641655}, {"title": "Suspiria", "show_id": "s4051", "similarity": 0.7215465108599116}, {"title": "The Sheik", "show_id": "s3726", "similarity": 0.7167594371495939}, {"title": "Operation Avalanche", "show_id": "s8106", "similarity": 0.7060483371270012}, {"title": "Trying Grace", "show_id": "s7595", "similarity": 0.7044626409776164}, {"title": "Shanghai Disney Resort Grand Opening Gala", "show_id": "s295", "similarity": 0.7035801295960805}, {"title": "The Legend of Swee' Pea", "show_id": "s565", "similarity": 0.6992762418494328}, {"title": "The Z Virus", "show_id": "s8732", "similarity": 0.6908215252006857}, {"title": "Extremis", "show_id": "s5798", "similarity": 0.6867877195283146}, {"title": "Journey to Royal: A WWII Rescue Mission", "show_id": "s4807", "similarity": 0.6830348539927185}, {"title": "Narbachi Wadi", "show_id": "s5879", "similarity": 0.6824890235942042}, {"title": "Daredevil DIRECTOR'S CUT", "show_id": "s8325", "similarity": 0.6810832334129707}, {"title": "Aurinko In Adagio: A Rising Voices Film", "show_id": "s5974", "similarity": 0.6756892776605048}, {"title": "Mad Money", "show_id": "s7370", "similarity": 0.6744356204785764}, {"title": "Anjelah Johnson: Not Fancy", "show_id": "s5895", "similarity": 0.6715562800569002}, {"title": "BELLATOR MMA: Kongo vs. Johnson 2", "show_id": "s38", "similarity": 0.6715470446362188}, {"title": "Knives Out", "show_id": "s5656", "similarity": 0.6690988586261262}]"
24
+ }
25
+ 📡 Querying table: mp7bnn9tzhojh7k
26
+
27
+ 🔍 Query params: {'where': '(show_id,eq,"s1")'}
28
+
29
+ 📥 Response status: 200
30
+
31
+ 📥 Request URL: https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/mp7bnn9tzhojh7k?where=%28show_id%2Ceq%2C%22s1%22%29
32
+
33
+ 🔄 Trying alternative query format...
34
+
35
+ 📥 Alt response status: 200
36
+
37
+ 📥 Alt request URL: https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/mp7bnn9tzhojh7k?where=%28show_id%2Clike%2C%22s1%22%29
38
+
39
+ Raw response:
40
+
41
+ {
42
+ "list":[]
43
+ "pageInfo":{
44
+ "totalRows":0
45
+ "page":1
46
+ "pageSize":25
47
+ "isFirstPage":true
48
+ "isLastPage":true
49
+ }
50
+ }
51
+ No entries found in response
52
+
53
+ Could not parse response as JSON: {"list":[],"pageInfo":{"totalRows":0,"page":1,"pageSize":25,"isFirstPage":true,"isLastPage":true}}
54
+
55
+ 📊 Found 0 matches in table
56
+
57
+ 📋 Table structure for m8e5rglns4acmef:
58
+
59
+ Columns:
60
+
61
+ [
62
+ 0:"Id"
63
+ 1:"CreatedAt"
64
+ 2:"UpdatedAt"
65
+ 3:"title"
66
+ 4:"show_id"
67
+ 5:"similar_items"
68
+ ]
69
+ Sample row:
70
+
71
+ {
72
+ "Id":1
73
+ "CreatedAt":"2025-06-02 09:34:10+00:00"
74
+ "UpdatedAt":NULL
75
+ "title":"Man Vs."
76
+ "show_id":"s1751"
77
+ "similar_items":"[{"title": "Settlers", "show_id": "s5", "similarity": 0.7325137795194292}, {"title": "Nature's Weirdest Events", "show_id": "s7575", "similarity": 0.7188299496186253}, {"title": "After Darkness", "show_id": "s2136", "similarity": 0.7141004186676579}, {"title": "Somnus", "show_id": "s6048", "similarity": 0.7099398640829218}, {"title": "Little John", "show_id": "s6801", "similarity": 0.7046743432606751}, {"title": "Shanghai Disney Resort Grand Opening Gala", "show_id": "s295", "similarity": 0.6926804611909125}, {"title": "Before I'm Dead", "show_id": "s5522", "similarity": 0.6857059160224453}, {"title": "Hostile", "show_id": "s4064", "similarity": 0.6842681482307864}, {"title": "The Haunted Hotel", "show_id": "s6793", "similarity": 0.6802541467049906}, {"title": "Star Wars: The Rise of Skywalker (Episode IX)", "show_id": "s531", "similarity": 0.6760391454742809}, {"title": "High-Rise Invasion", "show_id": "s1045", "similarity": 0.6680968736571052}, {"title": "Humans", "show_id": "s2168", "similarity": 0.6671485424786042}, {"title": "The Handmaid's Tale", "show_id": "s764", "similarity": 0.666126348250307}, {"title": "Adam Ruins Everything", "show_id": "s6090", "similarity": 0.6625023465211879}, {"title": "Coherence", "show_id": "s2812", "similarity": 0.6624407207293317}, {"title": "The Vast of Night", "show_id": "s403", "similarity": 0.6600187026296738}, {"title": "Five Grand", "show_id": "s6647", "similarity": 0.6577227564879091}, {"title": "Technotise: Edit & I", "show_id": "s4188", "similarity": 0.6560547168812791}, {"title": "Ghost Walk", "show_id": "s6779", "similarity": 0.6543972382095791}, {"title": "Beyond the Sky", "show_id": "s3086", "similarity": 0.6536638553019316}]"
78
+ }
79
+ 📡 Querying table: m8e5rglns4acmef
80
+
81
+ 🔍 Query params: {'where': '(show_id,eq,"s1")'}
82
+
83
+ 📥 Response status: 200
84
+
85
+ 📥 Request URL: https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/m8e5rglns4acmef?where=%28show_id%2Ceq%2C%22s1%22%29
86
+
87
+ 🔄 Trying alternative query format...
88
+
89
+ 📥 Alt response status: 200
90
+
91
+ 📥 Alt request URL: https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/m8e5rglns4acmef?where=%28show_id%2Clike%2C%22s1%22%29
92
+
93
+ Raw response:
94
+
95
+ {
96
+ "list":[]
97
+ "pageInfo":{
98
+ "totalRows":0
99
+ "page":1
100
+ "pageSize":25
101
+ "isFirstPage":true
102
+ "isLastPage":true
103
+ }
104
+ }
105
+ No entries found in response
106
+
107
+ Could not parse response as JSON: {"list":[],"pageInfo":{"totalRows":0,"page":1,"pageSize":25,"isFirstPage":true,"isLastPage":true}}
108
+
109
+ 📊 Found 0 matches in table
110
+
111
+ 📋 Table structure for m2driodimid10k6:
112
+
113
+ Columns:
114
+
115
+ [
116
+ 0:"Id"
117
+ 1:"CreatedAt"
118
+ 2:"UpdatedAt"
119
+ 3:"title"
120
+ 4:"show_id"
121
+ 5:"similar_items"
122
+ ]
123
+ Sample row:
124
+
125
+ {
126
+ "Id":1
127
+ "CreatedAt":"2025-06-02 09:34:09+00:00"
128
+ "UpdatedAt":NULL
129
+ "title":"Make This Tonight"
130
+ "show_id":"s1281"
131
+ "similar_items":"[{"title": "Cake Wars", "show_id": "s2428", "similarity": 0.8643776874920789}, {"title": "Tasty 101", "show_id": "s2270", "similarity": 0.851035239717973}, {"title": "Halloween Wars", "show_id": "s1356", "similarity": 0.839582130187393}, {"title": "Giada's Holiday Handbook", "show_id": "s1251", "similarity": 0.8264038541160613}, {"title": "On Chesil Beach", "show_id": "s212", "similarity": 0.821701576024188}, {"title": "Raw. Vegan. Not Gross.", "show_id": "s2018", "similarity": 0.8133997169642753}, {"title": "Crime Scene Kitchen", "show_id": "s684", "similarity": 0.8130779826834713}, {"title": "Worst Cooks in America", "show_id": "s592", "similarity": 0.8117540162729204}, {"title": "Cutthroat Kitchen", "show_id": "s587", "similarity": 0.8111139381056529}, {"title": "Lightened Up", "show_id": "s1280", "similarity": 0.8079789805365294}, {"title": "Chuck's World", "show_id": "s216", "similarity": 0.8049357265970711}, {"title": "Ayesha's Home Kitchen", "show_id": "s1240", "similarity": 0.8013048453237337}, {"title": "Struggle Meals", "show_id": "s1283", "similarity": 0.793547594830839}, {"title": "TrueSouth", "show_id": "s869", "similarity": 0.7906473317607992}, {"title": "Behind The Dish", "show_id": "s1279", "similarity": 0.7876233721299817}, {"title": "Eater's Guide to the World", "show_id": "s1212", "similarity": 0.7800859117925178}, {"title": "Supermarket Stakeout", "show_id": "s1367", "similarity": 0.7786342300343229}, {"title": "The Grill Iron", "show_id": "s1284", "similarity": 0.7781488386934403}, {"title": "F*ck, That's Delicious", "show_id": "s1308", "similarity": 0.7778297208530904}, {"title": "Jamie: Keep Cooking and Carry On", "show_id": "s1496", "similarity": 0.7756508905864695}]"
132
+ }
133
+ 📡 Querying table: m2driodimid10k6
134
+
135
+ 🔍 Query params: {'where': '(show_id,eq,"s1")'}
136
+
137
+ 📥 Response status: 200
138
+
139
+ 📥 Request URL: https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/m2driodimid10k6?where=%28show_id%2Ceq%2C%22s1%22%29
140
+
141
+ 🔄 Trying alternative query format...
142
+
143
+ 📥 Alt response status: 200
144
+
145
+ 📥 Alt request URL: https://mtoft20-potm.hf.space/api/v1/db/data/noco/p9pozkcw81t9aee/m2driodimid10k6?where=%28show_id%2Clike%2C%22s1%22%29
146
+
147
+ Raw response:
148
+
149
+ {
150
+ "list":[]
151
+ "pageInfo":{
152
+ "totalRows":0
153
+ "page":1
154
+ "pageSize":25
155
+ "isFirstPage":true
156
+ "isLastPage":true
157
+ }
158
+ }
159
+ No entries found in response
160
+
161
+ Could not parse response as JSON: {"list":[],"pageInfo":{"totalRows":0,"page":1,"pageSize":25,"isFirstPage":true,"isLastPage":true}}
162
+
163
+ 📊 Found 0 matches in table
164
+
165
+ 📝 Total similar items found: 0
166
+
167
+ No similar content found.