Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import requests | |
| from together import Together | |
| import os | |
| import json | |
| # ============================================================================= | |
| # CONFIGURATION - Using Secrets Management | |
| # ============================================================================= | |
| NOCODB_URL = "https://mtoft20-potm.hf.space".strip() # Base URL, ensure no extra spaces | |
| # Get sensitive data from Streamlit secrets or environment variables | |
| def get_api_credentials(): | |
| """Get API credentials from secrets or environment""" | |
| try: | |
| # Try Streamlit secrets first (for Hugging Face Spaces) | |
| api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", "")).strip() | |
| together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", "")).strip() | |
| # Get endpoints for content and similarities | |
| content_endpoint = st.secrets.get("NOCODB_CONTENT_ENDPOINT", os.environ.get("NOCODB_CONTENT_ENDPOINT", "")).strip() | |
| similarity_endpoints = [endpoint.strip() for endpoint in | |
| st.secrets.get("NOCODB_SIMILARITY_ENDPOINTS", | |
| os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "")).split(",") | |
| if endpoint.strip()] | |
| return api_token, together_key, content_endpoint, similarity_endpoints | |
| except: | |
| # Fallback to environment variables | |
| api_token = os.environ.get("NOCODB_API_TOKEN", "").strip() | |
| together_key = os.environ.get("TOGETHER_API_KEY", "").strip() | |
| content_endpoint = os.environ.get("NOCODB_CONTENT_ENDPOINT", "").strip() | |
| similarity_endpoints = [endpoint.strip() for endpoint in | |
| os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "").split(",") | |
| if endpoint.strip()] | |
| return api_token, together_key, content_endpoint, similarity_endpoints | |
| # Initialize Together AI client | |
| def get_ai_client(): | |
| """Initialize Together AI client""" | |
| _, together_key, _, _ = get_api_credentials() | |
| if not together_key: | |
| st.error("Together AI API key not found. Please configure it in the secrets.") | |
| return None | |
| return Together(api_key=together_key) | |
| # ============================================================================= | |
| # HELPER FUNCTIONS | |
| # ============================================================================= | |
| # Cache for 5 minutes | |
| def get_streaming_content(): | |
| """Fetch streaming content from NocoDB with pagination""" | |
| api_token, _, content_endpoint, _ = get_api_credentials() | |
| if not api_token or not content_endpoint: | |
| st.error("NocoDB credentials not configured. Please set up your secrets.") | |
| return [] | |
| headers = { | |
| "xc-token": api_token, | |
| "accept": "application/json" | |
| } | |
| all_content = [] | |
| page = 1 | |
| page_size = 1000 # NocoDB default page size | |
| try: | |
| while True: | |
| offset = (page - 1) * page_size | |
| url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}?limit={page_size}&offset={offset}" | |
| response = requests.get(url, headers=headers) | |
| if response.status_code == 200: | |
| data = response.json() | |
| current_page_data = data.get('list', []) | |
| if not current_page_data: # No more data to fetch | |
| break | |
| all_content.extend(current_page_data) | |
| # Check if this is the last page | |
| page_info = data.get('pageInfo', {}) | |
| if page_info.get('isLastPage', True): | |
| break | |
| page += 1 | |
| else: | |
| st.error(f"Failed to fetch data: {response.status_code}") | |
| if not all_content: # Only return [] if we haven't fetched any data | |
| return [] | |
| break # If we have some data, return what we've got | |
| return all_content | |
| except Exception as e: | |
| st.error(f"Error connecting to database: {str(e)}") | |
| st.write("Full error details:", e) | |
| return [] | |
| def filter_content(content_list, filters): | |
| """Apply filters to streaming content list""" | |
| filtered = [] | |
| for content in content_list: | |
| if not content or not isinstance(content, dict): | |
| continue | |
| matches_all_filters = True | |
| # Streaming service filter | |
| if filters['streaming_services']: | |
| if content.get('streaming_service') not in filters['streaming_services']: | |
| matches_all_filters = False | |
| continue | |
| # Type filter - only apply if not "All" | |
| if filters['content_type']: | |
| if content.get('type') != filters['content_type']: | |
| matches_all_filters = False | |
| continue | |
| # Genre filter - check if ALL selected genres are in the content's genres | |
| if filters['genres']: | |
| content_genres = set(g.strip().lower() for g in str(content.get('listed_in', '')).split(',')) | |
| selected_genres = set(g.strip().lower() for g in filters['genres']) | |
| if not selected_genres.issubset(content_genres): | |
| matches_all_filters = False | |
| continue | |
| # Rating filter | |
| if filters['ratings']: | |
| rating = (content.get('rating') or '').strip() | |
| # Only compare if rating is a valid string and not a duration | |
| if not rating or not isinstance(rating, str) or rating.endswith('min'): | |
| matches_all_filters = False | |
| continue | |
| if rating not in filters['ratings']: | |
| matches_all_filters = False | |
| continue | |
| # Release year filter | |
| try: | |
| release_year = int(content.get('release_year', 0)) | |
| if release_year < filters['year_range'][0] or release_year > filters['year_range'][1]: | |
| matches_all_filters = False | |
| continue | |
| except (ValueError, TypeError): | |
| matches_all_filters = False | |
| continue | |
| # Duration filter (different handling for movies) | |
| if filters['content_type'] == 'Movie': | |
| duration = str(content.get('duration', '')) | |
| if 'min' in duration: | |
| try: | |
| minutes = int(duration.split()[0]) | |
| if minutes < filters['duration_range'][0] or minutes > filters['duration_range'][1]: | |
| matches_all_filters = False | |
| continue | |
| except (ValueError, IndexError): | |
| matches_all_filters = False | |
| continue | |
| # Director filter (optional) | |
| if filters['director']: | |
| director = str(content.get('director', '')).lower() | |
| if not any(name.strip().lower() in director for name in filters['director'].split(',')): | |
| matches_all_filters = False | |
| continue | |
| # Cast filter (optional) | |
| if filters['cast']: | |
| cast = str(content.get('cast', '')).lower() | |
| if not any(name.strip().lower() in cast for name in filters['cast'].split(',')): | |
| matches_all_filters = False | |
| continue | |
| if matches_all_filters: | |
| filtered.append(content) | |
| return filtered | |
| def create_content_context(content_list): | |
| """Create context string about current content for AI""" | |
| if not content_list: | |
| return "No content matches the current filters." | |
| total = len(content_list) | |
| movies = sum(1 for c in content_list if c.get('type') == 'Movie') | |
| shows = sum(1 for c in content_list if c.get('type') == 'TV Show') | |
| context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows) """ | |
| # Add streaming services info | |
| services = set(c.get('streaming_service') for c in content_list if c.get('streaming_service')) | |
| if services: | |
| context += f"available on {', '.join(services)}. " | |
| return context | |
| def get_ai_response(client, question, context, model_name): | |
| """Get response from Together AI""" | |
| try: | |
| prompt = f"""You are a helpful streaming content expert. Based on the current content data, please answer the user's question accurately and helpfully. | |
| Current Content Data Context: | |
| {context} | |
| User Question: {question} | |
| Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative.""" | |
| response = client.chat.completions.create( | |
| model=model_name, | |
| messages=[ | |
| {"role": "system", "content": "You are a helpful content expert with deep knowledge of movies and TV shows."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=300, | |
| temperature=0.7, | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| raise Exception(f"Together AI Error: {str(e)}") | |
| def extract_unique_names(content_list, field): | |
| """Extract unique names from a comma-separated field in content list""" | |
| unique_names = set() | |
| for content in content_list: | |
| names = content.get(field, '') | |
| if names: | |
| # Split by comma and clean each name | |
| for name in names.split(','): | |
| cleaned_name = name.strip() | |
| if cleaned_name: # Only add non-empty names | |
| unique_names.add(cleaned_name) | |
| return sorted(list(unique_names)) | |
| def get_similar_content(content, n_recommendations=5): | |
| """Get pre-computed similar content from database""" | |
| try: | |
| # Get database credentials | |
| api_token, _, content_endpoint, similarity_endpoints = get_api_credentials() | |
| if not api_token or not similarity_endpoints: | |
| st.error("NocoDB credentials not configured properly.") | |
| return [] | |
| headers = { | |
| "xc-token": api_token, | |
| "accept": "application/json" | |
| } | |
| title = content.get('title', '') | |
| show_id = content.get('show_id', '') | |
| # Try finding by both show_id and title | |
| query = f'where=(show_id,eq,{show_id})~and(title,eq,{title})' | |
| params = { | |
| "where": query | |
| } | |
| for endpoint in similarity_endpoints: | |
| if not endpoint.strip(): # Skip empty endpoints | |
| continue | |
| try: | |
| url = f"{NOCODB_URL.strip()}{endpoint.strip()}" | |
| response = requests.get(url, headers=headers, params=params) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get('list'): | |
| for entry in data['list']: | |
| try: | |
| similar_items = json.loads(entry['similar_items']) | |
| # Get full content details for each similar item | |
| similar_content = [] | |
| for item in similar_items[:n_recommendations]: | |
| show_id = item.get('show_id', '') | |
| query = f'where=(show_id,eq,{show_id})' | |
| content_params = { | |
| "where": query | |
| } | |
| content_url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}" | |
| content_response = requests.get(content_url, headers=headers, params=content_params) | |
| if content_response.status_code == 200: | |
| content_data = content_response.json() | |
| if content_data and len(content_data.get('list', [])) > 0: | |
| content_dict = content_data['list'][0] | |
| content_dict['similarity'] = f"{item['similarity']:.2%}" | |
| similar_content.append(content_dict) | |
| return similar_content[:n_recommendations] | |
| except Exception as parse_error: | |
| continue | |
| except Exception as e: | |
| continue | |
| return [] | |
| except Exception as e: | |
| return [] | |
| # ============================================================================= | |
| # MAIN APP | |
| # ============================================================================= | |
| def main(): | |
| # Page config | |
| st.set_page_config( | |
| page_title="StreamButler - Your Personal Streaming Concierge", | |
| page_icon="π©", | |
| layout="wide" | |
| ) | |
| # Header with butler theme | |
| st.title("π© StreamButler") | |
| st.write("*At your service! Allow me to curate the perfect streaming entertainment for you.*") | |
| # Check API credentials | |
| api_token, together_key, content_endpoint, similarity_endpoints = get_api_credentials() | |
| if not together_key: | |
| st.error("β οΈ Together AI API key not configured!") | |
| st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.") | |
| st.stop() | |
| if not api_token or not content_endpoint: | |
| st.error("β οΈ NocoDB credentials not configured!") | |
| st.info("Please set NOCODB_API_TOKEN and NOCODB_CONTENT_ENDPOINT in the Hugging Face Spaces secrets.") | |
| st.stop() | |
| # Initialize AI client | |
| try: | |
| client = get_ai_client() | |
| if not client: | |
| st.stop() | |
| except Exception as e: | |
| st.error(f"Failed to initialize Together AI client: {e}") | |
| st.stop() | |
| # Load all content first | |
| with st.spinner("Loading streaming content..."): | |
| all_content = get_streaming_content() | |
| if not all_content: | |
| st.error("Could not load streaming content. Please check your NocoDB connection.") | |
| st.stop() | |
| # Extract unique values for filters | |
| all_ratings = sorted(list(set( | |
| c.get('rating') for c in all_content | |
| if c and isinstance(c, dict) | |
| and c.get('rating') | |
| and isinstance(c.get('rating'), str) | |
| and not c.get('rating').endswith('min') # Exclude duration values | |
| and c.get('rating').strip() # Exclude empty strings | |
| ))) | |
| all_genres = sorted(list(set( | |
| genre.strip() | |
| for c in all_content | |
| for genre in c.get('listed_in', '').split(',') | |
| if genre.strip() | |
| ))) | |
| all_streaming_services = sorted(list(set([c.get('streaming_service') for c in all_content if c.get('streaming_service')]))) | |
| # Extract unique directors and cast members | |
| all_directors = extract_unique_names(all_content, 'director') | |
| all_cast_members = extract_unique_names(all_content, 'cast') | |
| # Sidebar filters | |
| st.sidebar.header("π Filter Content") | |
| with st.sidebar.form("filter_form"): | |
| st.subheader("Streaming Services") | |
| # Streaming service selection (required) | |
| selected_services = st.multiselect( | |
| "Select Your Streaming Services", | |
| options=all_streaming_services, | |
| default=all_streaming_services[:1], # Default to first service | |
| help="Select the streaming services you have access to", | |
| key="streaming_services" | |
| ) | |
| if not selected_services: | |
| st.warning("Please select at least one streaming service") | |
| st.subheader("Content Filters") | |
| content_type = st.selectbox( | |
| "Content Type", | |
| options=["All", "Movie", "TV Show"], | |
| index=0 | |
| ) | |
| selected_genres = st.multiselect( | |
| "Genres", | |
| options=all_genres, | |
| default=[] | |
| ) | |
| st.subheader("Optional Filters") | |
| # Rating filter | |
| selected_ratings = st.multiselect( | |
| "Ratings", | |
| options=all_ratings, | |
| default=[], | |
| help="Filter by content rating" | |
| ) | |
| # Year range slider | |
| years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')] | |
| min_year, max_year = min(years), max(years) | |
| year_range = st.slider( | |
| "Release Year", | |
| min_value=min_year, | |
| max_value=max_year, | |
| value=(min_year, max_year), | |
| help="Filter by release year range" | |
| ) | |
| # Duration range slider (for movies only) | |
| movie_durations = [ | |
| int(str(c.get('duration', '0 min')).split()[0]) | |
| for c in all_content | |
| if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', '')) | |
| ] | |
| if movie_durations: | |
| min_duration = min(d for d in movie_durations if d > 0) | |
| max_duration = max(movie_durations) | |
| duration_range = st.slider( | |
| "Movie Duration (minutes)", | |
| min_value=min_duration, | |
| max_value=max_duration, | |
| value=(min_duration, max_duration), | |
| help="This filter only applies to movies" | |
| ) | |
| else: | |
| duration_range = (0, 1000) # Fallback values | |
| # Director filter with autocomplete | |
| selected_directors = st.multiselect( | |
| "Directors", | |
| options=all_directors, | |
| default=[], | |
| help="Select one or more directors (searchable)", | |
| placeholder="Start typing to search directors..." | |
| ) | |
| # Cast filter with autocomplete | |
| selected_cast = st.multiselect( | |
| "Cast Members", | |
| options=all_cast_members, | |
| default=[], | |
| help="Select one or more cast members (searchable)", | |
| placeholder="Start typing to search cast members..." | |
| ) | |
| # Submit button | |
| apply_filters = st.form_submit_button("π Apply Filters", type="primary") | |
| # Create filter dictionary | |
| filters = { | |
| 'streaming_services': selected_services, | |
| 'content_type': content_type if content_type != "All" else None, | |
| 'ratings': selected_ratings, | |
| 'genres': selected_genres, | |
| 'year_range': year_range, | |
| 'duration_range': duration_range, | |
| 'director': ','.join(selected_directors) if selected_directors else '', | |
| 'cast': ','.join(selected_cast) if selected_cast else '' | |
| } | |
| # Only apply filters when the button is clicked | |
| if apply_filters: | |
| filtered_content = filter_content(all_content, filters) | |
| st.session_state.filtered_content = filtered_content | |
| else: | |
| # Initialize filtered content if not exists | |
| if 'filtered_content' not in st.session_state: | |
| st.session_state.filtered_content = all_content | |
| # Main content area | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| # Content listings | |
| filtered_count = len(st.session_state.filtered_content) | |
| if filtered_count == 0: | |
| st.subheader("π No Titles Found") | |
| else: | |
| # Header with count and page info | |
| st.subheader(f"π Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}") | |
| if st.session_state.filtered_content: | |
| # Active Filters section with better formatting | |
| if any([filters['content_type'], filters['genres'], filters['ratings'], | |
| filters['director'], filters['cast']]): | |
| with st.expander("π Active Filters", expanded=True): | |
| filter_cols = st.columns(2) | |
| with filter_cols[0]: | |
| if filters['content_type']: | |
| st.write(f"**Type:** {filters['content_type']}") | |
| if filters['genres']: | |
| st.write(f"**Genres:** {', '.join(filters['genres'])}") | |
| if filters['ratings']: | |
| st.write(f"**Ratings:** {', '.join(filters['ratings'])}") | |
| with filter_cols[1]: | |
| if filters['director']: | |
| st.write(f"**Director:** {filters['director']}") | |
| if filters['cast']: | |
| st.write(f"**Cast:** {filters['cast']}") | |
| st.write("---") | |
| # Pagination setup | |
| items_per_page = 10 | |
| total_pages = (filtered_count + items_per_page - 1) // items_per_page | |
| # Initialize page number in session state if not exists | |
| if 'current_page' not in st.session_state: | |
| st.session_state.current_page = 1 | |
| # Calculate slice indices for current page | |
| start_idx = (st.session_state.current_page - 1) * items_per_page | |
| end_idx = min(start_idx + items_per_page, filtered_count) | |
| # Display current range info | |
| st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles") | |
| # Show items for current page | |
| for i, content in enumerate(st.session_state.filtered_content[start_idx:end_idx], start=start_idx): | |
| with st.container(): | |
| st.write(f"### {content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})") | |
| # Content details in columns | |
| detail_col1, detail_col2 = st.columns(2) | |
| with detail_col1: | |
| st.write(f"**πΊ Available on:** {content.get('streaming_service', 'N/A')}") | |
| st.write(f"**π Type:** {content.get('type', 'N/A')}") | |
| st.write(f"**β Rating:** {content.get('rating', 'N/A')}") | |
| st.write(f"**β±οΈ Duration:** {content.get('duration', 'N/A')}") | |
| with detail_col2: | |
| st.write(f"**π¬ Genres:** {content.get('listed_in', 'N/A')}") | |
| cast = content.get('cast') | |
| cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A" | |
| st.write(f"**π₯ Cast:** {cast_display}") | |
| st.write(f"**π Director:** {content.get('director', 'N/A')}") | |
| # Description | |
| st.write(f"**π Description:**") | |
| st.write(content.get('description', 'N/A')) | |
| # Add Find Similar button with loading state | |
| similar_button = st.button(f"π Find Similar Content", key=f"similar_{i}") | |
| if similar_button: | |
| with st.spinner("Finding similar content..."): | |
| similar_content = get_similar_content(content, n_recommendations=5) | |
| if similar_content: | |
| # Create tabs for different aspects of recommendations | |
| sim_tab1, sim_tab2 = st.tabs(["πΊ Similar Titles", "π Why These Recommendations"]) | |
| with sim_tab1: | |
| for sim_content in similar_content[:5]: # Show top 5 similar items | |
| with st.container(): | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})") | |
| st.write(f"*Available on:* {sim_content.get('streaming_service')}") | |
| st.write(f"*Genres:* {sim_content.get('listed_in')}") | |
| st.write(f"*Cast:* {sim_content.get('cast')}") | |
| st.write(f"*Director:* {sim_content.get('director')}") | |
| st.write(f"*Description:* {sim_content.get('description')}") | |
| with col2: | |
| st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}") | |
| st.write("---") | |
| with sim_tab2: | |
| st.write("**Why these recommendations?**") | |
| st.write(""" | |
| These recommendations are based on multiple factors: | |
| - Genre and theme matching | |
| - Plot similarity analysis | |
| - Cast and director relationships | |
| - Release year proximity | |
| The percentage match indicates how similar each title is to your selection. | |
| """) | |
| else: | |
| st.info("No similar content found.") | |
| st.write("---") | |
| # Bottom pagination controls with better layout | |
| st.write("---") | |
| page_cols = st.columns([1, 2, 1, 2, 1]) | |
| # Previous button | |
| with page_cols[0]: | |
| if st.button("β Previous", disabled=st.session_state.current_page == 1, use_container_width=True): | |
| st.session_state.current_page -= 1 | |
| st.rerun() | |
| # Spacer | |
| with page_cols[1]: | |
| st.write("") | |
| # Page input | |
| with page_cols[2]: | |
| page_input = st.number_input( | |
| f"Page (of {total_pages})", | |
| min_value=1, | |
| max_value=total_pages, | |
| value=st.session_state.current_page, | |
| key="page_number", | |
| help=f"Enter a page number between 1 and {total_pages}" | |
| ) | |
| if page_input != st.session_state.current_page: | |
| st.session_state.current_page = page_input | |
| st.rerun() | |
| # Spacer | |
| with page_cols[3]: | |
| st.write("") | |
| # Next button | |
| with page_cols[4]: | |
| if st.button("Next β", disabled=st.session_state.current_page == total_pages, use_container_width=True): | |
| st.session_state.current_page += 1 | |
| st.rerun() | |
| else: | |
| st.info("No content matches your current filters. Try adjusting the criteria.") | |
| with col2: | |
| # AI Chat Section | |
| st.subheader("π© Your Personal Butler") | |
| st.write("How may I be of assistance in finding your perfect entertainment today?") | |
| # Model selection for Together AI | |
| model_choice = st.selectbox( | |
| "Select Your Butler's Expertise Level:", | |
| [ | |
| "google/gemma-2b-it", | |
| "google/gemma-2-27b-it", | |
| "mistralai/Mistral-7B-Instruct-v0.1", | |
| "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", | |
| "mistralai/Mixtral-8x7B-Instruct-v0.1" | |
| ], | |
| help="Select your butler's level of expertise in making recommendations" | |
| ) | |
| # Example questions | |
| with st.expander("π‘ How to Address Your Butler"): | |
| st.write(""" | |
| Your butler understands requests like: | |
| β’ "My good sir, I seek an action film that would also please my companion who favors comedies." | |
| β’ "Would you be so kind as to suggest a family-friendly show in the spirit of Stranger Things, but less frightening?" | |
| β’ "I've quite enjoyed The Crown and Downton Abbey. Might you recommend similar period dramas?" | |
| β’ "The weather is rather gloomy today. Perhaps a charming romantic comedy or musical?" | |
| β’ "I'm in search of enlightening documentaries about technology or artificial intelligence." | |
| β’ "We're hosting a gathering this evening. What entertainment would you suggest for a group?" | |
| """) | |
| user_question = st.text_area( | |
| "How May I Assist You?", | |
| placeholder="Tell me your preferences, and I shall curate the perfect selection...", | |
| height=100 | |
| ) | |
| if st.button("π© Request Recommendations", type="primary"): | |
| if user_question: | |
| with st.spinner("Your butler is carefully selecting the perfect entertainment..."): | |
| # Create context from current filtered data | |
| context = create_content_context(st.session_state.filtered_content) | |
| try: | |
| # Get AI response | |
| ai_response = get_ai_response(client, user_question, context, model_choice) | |
| st.success("**π© Your Curated Selection:**") | |
| st.write(ai_response) | |
| # Show streaming availability | |
| with st.expander("π© Butler's Note"): | |
| st.write(""" | |
| To access your selected entertainment: | |
| 1. Kindly select your preferred streaming services above | |
| 2. Locate your chosen title in the curated list | |
| 3. For similar recommendations, simply request "Find Similar Content" | |
| *Is there anything else I can assist you with?* | |
| """) | |
| except Exception as e: | |
| st.error("My sincerest apologies, but I seem to be unable to process your request at the moment. Might we try again?") | |
| else: | |
| st.warning("How may I be of assistance? Please share your entertainment preferences.") | |
| # Footer stats with butler theme | |
| st.markdown("---") | |
| if all_content: | |
| total_items = len(all_content) | |
| filtered_items = len(st.session_state.filtered_content) | |
| st.markdown("### π© Your Entertainment Library") | |
| # Create columns for stats with better spacing | |
| stat_cols = st.columns(len(selected_services) + 3) | |
| # Basic stats with improved formatting | |
| with stat_cols[0]: | |
| st.metric("π Complete Collection", f"{total_items:,}") | |
| with stat_cols[1]: | |
| st.metric("π― Curated Selection", f"{filtered_items:,}") | |
| with stat_cols[2]: | |
| movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie') | |
| shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show') | |
| st.metric("π¬ Films / πΊ Series", f"{movies:,} / {shows:,}") | |
| # Streaming service breakdown with icons | |
| service_icons = { | |
| "Netflix": "π΄", | |
| "Amazon Prime": "π΅", | |
| "Hulu": "π’", | |
| "Disney+": "π£" | |
| } | |
| for i, service in enumerate(selected_services, 3): | |
| if i < len(stat_cols): | |
| service_count = sum(1 for c in st.session_state.filtered_content if c.get('streaming_service') == service) | |
| icon = service_icons.get(service, "πΊ") | |
| with stat_cols[i]: | |
| st.metric(f"{icon} {service}", f"{service_count:,}") | |
| if __name__ == "__main__": | |
| main() |