mtoft20 commited on
Commit
702e195
Β·
verified Β·
1 Parent(s): 8900764

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +852 -167
src/streamlit_app.py CHANGED
@@ -1,167 +1,852 @@
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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 first
295
+ query = f'(show_id,eq,"{show_id}")'
296
+ params = {
297
+ "where": query
298
+ }
299
+
300
+ # Debug: Print the request details
301
+ st.write(f"πŸ“‘ Querying table: {table_url.split('/')[-1]}")
302
+ st.write(f"πŸ” Query params: {params}")
303
+
304
+ # Try first query format
305
+ try:
306
+ response = requests.get(table_url, headers=headers, params=params)
307
+ st.write(f"πŸ“₯ Response status: {response.status_code}")
308
+ st.write("πŸ“₯ Request URL:", response.url)
309
+
310
+ if response.status_code == 200:
311
+ data = response.json()
312
+ if not data.get('list'):
313
+ # If no results with show_id, try by title
314
+ st.write("πŸ”„ No results with show_id, trying title search...")
315
+ title_query = f'(title,eq,"{title}")'
316
+ title_params = {
317
+ "where": title_query
318
+ }
319
+ response = requests.get(table_url, headers=headers, params=title_params)
320
+ st.write(f"πŸ“₯ Title search response status: {response.status_code}")
321
+ st.write("πŸ“₯ Title search URL:", response.url)
322
+ if response.status_code == 200:
323
+ data = response.json()
324
+
325
+ # Debug: Print raw response
326
+ try:
327
+ response_json = response.json()
328
+ st.write("Raw response:", response_json)
329
+
330
+ # Additional debugging
331
+ if response_json.get('list'):
332
+ st.write("βœ… Found entries in response")
333
+ for entry in response_json['list']:
334
+ st.write(f"Entry title: {entry.get('title')}, show_id: {entry.get('show_id')}")
335
+ else:
336
+ st.write("❌ No entries found in response")
337
+
338
+ # Try to get a few random entries to verify data
339
+ sample_params = {
340
+ "limit": 3,
341
+ "shuffle": True
342
+ }
343
+ sample_response = requests.get(table_url, headers=headers, params=sample_params)
344
+ if sample_response.status_code == 200:
345
+ sample_data = sample_response.json()
346
+ st.write("πŸ” Random entries from table:")
347
+ for entry in sample_data.get('list', []):
348
+ st.write(f"- {entry.get('title')} (ID: {entry.get('show_id')})")
349
+
350
+ except Exception as e:
351
+ st.write("Could not parse response as JSON:", response.text)
352
+
353
+ if response.status_code == 200:
354
+ data = response_json
355
+ st.write(f"πŸ“Š Found {len(data.get('list', []))} matches in table")
356
+
357
+ if data and len(data.get('list', [])) > 0:
358
+ # Get similar items from stored data
359
+ try:
360
+ table_items = json.loads(data['list'][0]['similar_items'])
361
+ st.write(f"βœ… Successfully parsed {len(table_items)} similar items")
362
+ similar_items.extend(table_items)
363
+ except Exception as parse_error:
364
+ st.write(f"❌ Error parsing similar items: {str(parse_error)}")
365
+ st.write("Raw data:", data['list'][0])
366
+ except Exception as e:
367
+ st.write(f"❌ Request error: {str(e)}")
368
+
369
+ st.write(f"πŸ“ Total similar items found: {len(similar_items)}")
370
+
371
+ if similar_items:
372
+ # Sort by similarity score and limit to requested number
373
+ similar_items.sort(key=lambda x: x['similarity'], reverse=True)
374
+ similar_items = similar_items[:n_recommendations]
375
+
376
+ # Get full content details for each similar item
377
+ similar_content = []
378
+ for item in similar_items:
379
+ # Query main content table for full details using show_id if available
380
+ show_id = item.get('show_id', '')
381
+ if show_id:
382
+ query = f'(show_id,eq,"{show_id}")'
383
+ else:
384
+ # Fallback to title if show_id not available
385
+ query = f'(title,eq,"{item["title"]}")'
386
+
387
+ content_params = {
388
+ "where": query
389
+ }
390
+ content_response = requests.get(NOCODB_URL, headers=headers, params=content_params)
391
+
392
+ if content_response.status_code == 200:
393
+ content_data = content_response.json()
394
+ if content_data and len(content_data.get('list', [])) > 0:
395
+ content_dict = content_data['list'][0]
396
+ content_dict['similarity'] = f"{item['similarity']:.2%}"
397
+ similar_content.append(content_dict)
398
+ st.write(f"βœ… Found details for: {item['title']}")
399
+ else:
400
+ st.write(f"❌ No content details found for: {item['title']}")
401
+
402
+ return similar_content
403
+
404
+ return []
405
+ except Exception as e:
406
+ st.error(f"Error fetching similar content: {str(e)}")
407
+ st.write("Full error details:", e)
408
+ return []
409
+
410
+ # =============================================================================
411
+ # MAIN APP
412
+ # =============================================================================
413
+ def main():
414
+ # Page config
415
+ st.set_page_config(
416
+ page_title="Streaming Content Explorer",
417
+ page_icon="🎬",
418
+ layout="wide"
419
+ )
420
+
421
+ # Header
422
+ st.title("🎬 Streaming Content Explorer")
423
+ st.write("Explore movies and TV shows across multiple streaming platforms!")
424
+
425
+ # Check API credentials
426
+ api_token, together_key = get_api_credentials()
427
+
428
+ if not together_key:
429
+ st.error("⚠️ Together AI API key not configured!")
430
+ st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
431
+ st.stop()
432
+
433
+ if not api_token:
434
+ st.error("⚠️ NocoDB credentials not configured!")
435
+ st.info("Please set NOCODB_API_TOKEN in the Hugging Face Spaces secrets.")
436
+ st.stop()
437
+
438
+ # Initialize AI client
439
+ try:
440
+ client = get_ai_client()
441
+ if not client:
442
+ st.stop()
443
+ except Exception as e:
444
+ st.error(f"Failed to initialize Together AI client: {e}")
445
+ st.stop()
446
+
447
+ # Load all content first
448
+ with st.spinner("Loading streaming content..."):
449
+ all_content = get_streaming_content()
450
+
451
+ if not all_content:
452
+ st.error("Could not load streaming content. Please check your NocoDB connection.")
453
+ st.stop()
454
+
455
+ # Extract unique values for filters
456
+ all_ratings = sorted(list(set(
457
+ c.get('rating') for c in all_content
458
+ if c and isinstance(c, dict)
459
+ and c.get('rating')
460
+ and isinstance(c.get('rating'), str)
461
+ and not c.get('rating').endswith('min') # Exclude duration values
462
+ and c.get('rating').strip() # Exclude empty strings
463
+ )))
464
+ all_genres = sorted(list(set(
465
+ genre.strip()
466
+ for c in all_content
467
+ for genre in c.get('listed_in', '').split(',')
468
+ if genre.strip()
469
+ )))
470
+ all_streaming_services = sorted(list(set([c.get('streaming_service') for c in all_content if c.get('streaming_service')])))
471
+
472
+ # Extract unique directors and cast members
473
+ all_directors = extract_unique_names(all_content, 'director')
474
+ all_cast_members = extract_unique_names(all_content, 'cast')
475
+
476
+ # Sidebar filters
477
+ st.sidebar.header("πŸ” Filter Content")
478
+
479
+ with st.sidebar.form("filter_form"):
480
+ st.subheader("Streaming Services")
481
+
482
+ # Streaming service selection (required)
483
+ selected_services = st.multiselect(
484
+ "Select Your Streaming Services",
485
+ options=all_streaming_services,
486
+ default=all_streaming_services[:1], # Default to first service
487
+ help="Select the streaming services you have access to",
488
+ key="streaming_services"
489
+ )
490
+
491
+ if not selected_services:
492
+ st.warning("Please select at least one streaming service")
493
+
494
+ st.subheader("Content Filters")
495
+
496
+ content_type = st.selectbox(
497
+ "Content Type",
498
+ options=["All", "Movie", "TV Show"],
499
+ index=0
500
+ )
501
+
502
+ selected_genres = st.multiselect(
503
+ "Genres",
504
+ options=all_genres,
505
+ default=[]
506
+ )
507
+
508
+ st.subheader("Optional Filters")
509
+
510
+ # Rating filter
511
+ selected_ratings = st.multiselect(
512
+ "Ratings",
513
+ options=all_ratings,
514
+ default=[],
515
+ help="Filter by content rating"
516
+ )
517
+
518
+ # Year range slider
519
+ years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
520
+ min_year, max_year = min(years), max(years)
521
+ year_range = st.slider(
522
+ "Release Year",
523
+ min_value=min_year,
524
+ max_value=max_year,
525
+ value=(min_year, max_year),
526
+ help="Filter by release year range"
527
+ )
528
+
529
+ # Duration range slider (for movies only)
530
+ movie_durations = [
531
+ int(str(c.get('duration', '0 min')).split()[0])
532
+ for c in all_content
533
+ if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
534
+ ]
535
+
536
+ if movie_durations:
537
+ min_duration = min(d for d in movie_durations if d > 0)
538
+ max_duration = max(movie_durations)
539
+ duration_range = st.slider(
540
+ "Movie Duration (minutes)",
541
+ min_value=min_duration,
542
+ max_value=max_duration,
543
+ value=(min_duration, max_duration),
544
+ help="This filter only applies to movies"
545
+ )
546
+ else:
547
+ duration_range = (0, 1000) # Fallback values
548
+
549
+ # Director filter with autocomplete
550
+ selected_directors = st.multiselect(
551
+ "Directors",
552
+ options=all_directors,
553
+ default=[],
554
+ help="Select one or more directors (searchable)",
555
+ placeholder="Start typing to search directors..."
556
+ )
557
+
558
+ # Cast filter with autocomplete
559
+ selected_cast = st.multiselect(
560
+ "Cast Members",
561
+ options=all_cast_members,
562
+ default=[],
563
+ help="Select one or more cast members (searchable)",
564
+ placeholder="Start typing to search cast members..."
565
+ )
566
+
567
+ # Submit button
568
+ apply_filters = st.form_submit_button("πŸ” Apply Filters", type="primary")
569
+
570
+ # Create filter dictionary
571
+ filters = {
572
+ 'streaming_services': selected_services,
573
+ 'content_type': content_type if content_type != "All" else None,
574
+ 'ratings': selected_ratings,
575
+ 'genres': selected_genres,
576
+ 'year_range': year_range,
577
+ 'duration_range': duration_range,
578
+ 'director': ','.join(selected_directors) if selected_directors else '',
579
+ 'cast': ','.join(selected_cast) if selected_cast else ''
580
+ }
581
+
582
+ # Only apply filters when the button is clicked
583
+ if apply_filters:
584
+ filtered_content = filter_content(all_content, filters)
585
+ st.session_state.filtered_content = filtered_content
586
+ else:
587
+ # Initialize filtered content if not exists
588
+ if 'filtered_content' not in st.session_state:
589
+ st.session_state.filtered_content = all_content
590
+
591
+ # Main content area
592
+ col1, col2 = st.columns([2, 1])
593
+
594
+ with col1:
595
+ # Content listings
596
+ filtered_count = len(st.session_state.filtered_content)
597
+ if filtered_count == 0:
598
+ st.subheader("πŸ“‹ No Titles Found")
599
+ else:
600
+ # Header with count and page info
601
+ st.subheader(f"πŸ“‹ Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
602
+
603
+ if st.session_state.filtered_content:
604
+ # Active Filters section with better formatting
605
+ if any([filters['content_type'], filters['genres'], filters['ratings'],
606
+ filters['director'], filters['cast']]):
607
+ with st.expander("πŸ” Active Filters", expanded=True):
608
+ filter_cols = st.columns(2)
609
+ with filter_cols[0]:
610
+ if filters['content_type']:
611
+ st.write(f"**Type:** {filters['content_type']}")
612
+ if filters['genres']:
613
+ st.write(f"**Genres:** {', '.join(filters['genres'])}")
614
+ if filters['ratings']:
615
+ st.write(f"**Ratings:** {', '.join(filters['ratings'])}")
616
+ with filter_cols[1]:
617
+ if filters['director']:
618
+ st.write(f"**Director:** {filters['director']}")
619
+ if filters['cast']:
620
+ st.write(f"**Cast:** {filters['cast']}")
621
+ st.write("---")
622
+
623
+ # Pagination setup
624
+ items_per_page = 10
625
+ total_pages = (filtered_count + items_per_page - 1) // items_per_page
626
+
627
+ # Initialize page number in session state if not exists
628
+ if 'current_page' not in st.session_state:
629
+ st.session_state.current_page = 1
630
+
631
+ # Calculate slice indices for current page
632
+ start_idx = (st.session_state.current_page - 1) * items_per_page
633
+ end_idx = min(start_idx + items_per_page, filtered_count)
634
+
635
+ # Display current range info
636
+ st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
637
+
638
+ # Show items for current page
639
+ for i, content in enumerate(st.session_state.filtered_content[start_idx:end_idx], start=start_idx):
640
+ with st.expander(f"{content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})"):
641
+ # Content details in columns
642
+ detail_col1, detail_col2 = st.columns(2)
643
+
644
+ with detail_col1:
645
+ st.write(f"**πŸ“Ί Available on:** {content.get('streaming_service', 'N/A')}")
646
+ st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
647
+ st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
648
+ st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
649
+
650
+ with detail_col2:
651
+ st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
652
+ cast = content.get('cast')
653
+ cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
654
+ st.write(f"**πŸ‘₯ Cast:** {cast_display}")
655
+ st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
656
+
657
+ # Description
658
+ st.write(f"**πŸ“– Description:**")
659
+ st.write(content.get('description', 'N/A'))
660
+
661
+ # Add Find Similar button with loading state
662
+ similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{i}")
663
+ if similar_button:
664
+ with st.spinner("Finding similar content..."):
665
+ similar_content = get_similar_content(content)
666
+
667
+ if similar_content:
668
+ # Create tabs for different aspects of recommendations
669
+ sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
670
+
671
+ with sim_tab1:
672
+ for sim_content in similar_content:
673
+ with st.container():
674
+ col1, col2 = st.columns([3, 1])
675
+ with col1:
676
+ st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
677
+ st.write(f"*Available on:* {sim_content.get('streaming_service')}")
678
+ st.write(f"*Genres:* {sim_content.get('listed_in')}")
679
+ with col2:
680
+ st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
681
+
682
+ with st.expander("See more details"):
683
+ st.write(f"**Cast:** {sim_content.get('cast', 'N/A')}")
684
+ st.write(f"**Director:** {sim_content.get('director', 'N/A')}")
685
+ st.write(f"**Description:** {sim_content.get('description', 'N/A')}")
686
+ st.write("---")
687
+
688
+ with sim_tab2:
689
+ st.write("**Why these recommendations?**")
690
+ st.write("""
691
+ These recommendations are pre-computed using advanced content analysis:
692
+ - Genre and theme matching
693
+ - Plot similarity analysis
694
+ - Cast and director relationships
695
+ - Release year proximity
696
+
697
+ The percentage match indicates how similar each title is to your selection.
698
+ """)
699
+ else:
700
+ st.info("No similar content found.")
701
+
702
+ # Bottom pagination controls with better layout
703
+ st.write("---")
704
+ page_cols = st.columns([1, 2, 1, 2, 1])
705
+
706
+ # Previous button
707
+ with page_cols[0]:
708
+ if st.button("← Previous", disabled=st.session_state.current_page == 1, use_container_width=True):
709
+ st.session_state.current_page -= 1
710
+ st.rerun()
711
+
712
+ # Spacer
713
+ with page_cols[1]:
714
+ st.write("")
715
+
716
+ # Page input
717
+ with page_cols[2]:
718
+ page_input = st.number_input(
719
+ f"Page (of {total_pages})",
720
+ min_value=1,
721
+ max_value=total_pages,
722
+ value=st.session_state.current_page,
723
+ key="page_number",
724
+ help=f"Enter a page number between 1 and {total_pages}"
725
+ )
726
+ if page_input != st.session_state.current_page:
727
+ st.session_state.current_page = page_input
728
+ st.rerun()
729
+
730
+ # Spacer
731
+ with page_cols[3]:
732
+ st.write("")
733
+
734
+ # Next button
735
+ with page_cols[4]:
736
+ if st.button("Next β†’", disabled=st.session_state.current_page == total_pages, use_container_width=True):
737
+ st.session_state.current_page += 1
738
+ st.rerun()
739
+ else:
740
+ st.info("No content matches your current filters. Try adjusting the criteria.")
741
+
742
+ with col2:
743
+ # AI Chat Section
744
+ st.subheader("πŸ€– Ask AI Assistant")
745
+ st.write("Ask questions about the streaming content!")
746
+
747
+ # Model selection for Together AI
748
+ model_choice = st.selectbox(
749
+ "Select AI Model:",
750
+ [
751
+ "google/gemma-2b-it",
752
+ "google/gemma-2-27b-it",
753
+ "mistralai/Mistral-7B-Instruct-v0.1",
754
+ "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
755
+ "mistralai/Mixtral-8x7B-Instruct-v0.1"
756
+ ],
757
+ help="Select an AI model to answer your questions"
758
+ )
759
+
760
+ # Example questions
761
+ with st.expander("πŸ’‘ Example Questions"):
762
+ st.write("β€’ What are the most common genres?")
763
+ st.write("β€’ What's the average movie duration?")
764
+ st.write("β€’ Which directors have the most content?")
765
+ st.write("β€’ What are the trends in content ratings?")
766
+ st.write("β€’ Tell me about the release year distribution")
767
+
768
+ user_question = st.text_area(
769
+ "Your Question:",
770
+ placeholder="Ask about genres, ratings, directors, trends...",
771
+ height=100
772
+ )
773
+
774
+ if st.button("πŸ” Ask AI", type="primary"):
775
+ if user_question:
776
+ with st.spinner("AI is analyzing the content..."):
777
+ # Create context from current filtered data
778
+ context = create_content_context(st.session_state.filtered_content)
779
+
780
+ try:
781
+ # Get AI response
782
+ ai_response = get_ai_response(client, user_question, context, model_choice)
783
+
784
+ st.success("**AI Assistant Response:**")
785
+ st.write(ai_response)
786
+
787
+ # Show debug info
788
+ with st.expander("Debug Info"):
789
+ st.write(f"Model used: {model_choice}")
790
+ st.write(f"Content items analyzed: {len(st.session_state.filtered_content)}")
791
+ st.write(f"Context: {context[:150]}...")
792
+
793
+ except Exception as e:
794
+ st.error(f"AI Error: {str(e)}")
795
+
796
+ # Fallback response with data analysis
797
+ st.info("**Fallback Analysis:**")
798
+ if st.session_state.filtered_content:
799
+ movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
800
+ shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
801
+ st.write(f"β€’ Found {len(st.session_state.filtered_content)} titles ({movies} movies, {shows} TV shows)")
802
+
803
+ # Show top genres
804
+ genres = [g.strip() for c in st.session_state.filtered_content for g in c.get('listed_in', '').split(',')]
805
+ genre_counts = pd.Series(genres).value_counts()
806
+ st.write(f"β€’ Top genres: {', '.join(genre_counts.head(3).index)}")
807
+
808
+ # Show year range
809
+ years = [int(c.get('release_year', 0)) for c in st.session_state.filtered_content if c.get('release_year')]
810
+ if years:
811
+ st.write(f"β€’ Release years: {min(years)} - {max(years)}")
812
+ else:
813
+ st.warning("Please enter a question first!")
814
+
815
+ # Footer stats with improved layout
816
+ st.markdown("---")
817
+ if all_content:
818
+ total_items = len(all_content)
819
+ filtered_items = len(st.session_state.filtered_content)
820
+
821
+ # Create columns for stats with better spacing
822
+ stat_cols = st.columns(len(selected_services) + 3)
823
+
824
+ # Basic stats with improved formatting
825
+ with stat_cols[0]:
826
+ st.metric("πŸ“Š Total Available", f"{total_items:,}")
827
+
828
+ with stat_cols[1]:
829
+ st.metric("πŸ” Filtered Results", f"{filtered_items:,}")
830
+
831
+ with stat_cols[2]:
832
+ movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
833
+ shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
834
+ st.metric("🎬 Movies / πŸ“Ί Shows", f"{movies:,} / {shows:,}")
835
+
836
+ # Streaming service breakdown with icons
837
+ service_icons = {
838
+ "Netflix": "πŸ”΄",
839
+ "Amazon Prime": "πŸ”΅",
840
+ "Hulu": "🟒",
841
+ "Disney+": "🟣"
842
+ }
843
+
844
+ for i, service in enumerate(selected_services, 3):
845
+ if i < len(stat_cols):
846
+ service_count = sum(1 for c in st.session_state.filtered_content if c.get('streaming_service') == service)
847
+ icon = service_icons.get(service, "πŸ“Ί")
848
+ with stat_cols[i]:
849
+ st.metric(f"{icon} {service}", f"{service_count:,}")
850
+
851
+ if __name__ == "__main__":
852
+ main()