Spaces:
Running
Running
| import os | |
| import json | |
| import threading | |
| import time | |
| from datetime import datetime, timezone | |
| from flask import Flask, request, jsonify, render_template_string | |
| from flask_cors import CORS | |
| from werkzeug.serving import run_simple | |
| import logging | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = Flask(__name__) | |
| app.config['SECRET_KEY'] = 'asdf#FGSgvasgf$5$WGT' | |
| # Enable CORS for all routes | |
| CORS(app) | |
| # Enhanced data storage with chat functionality and better time handling | |
| class EnhancedDataStore: | |
| def __init__(self, backup_file='enhanced_data_backup.json'): | |
| self.backup_file = backup_file | |
| self.data = { | |
| 'posts': [], | |
| 'comments': [], | |
| 'chat_messages': [], | |
| 'next_post_id': 1, | |
| 'next_comment_id': 1, | |
| 'next_chat_id': 1, | |
| 'app_version': '2.0.0', | |
| 'last_updated': datetime.utcnow().isoformat() | |
| } | |
| self.lock = threading.Lock() | |
| self.load_data() | |
| # Start background backup thread | |
| self.backup_thread = threading.Thread(target=self.periodic_backup, daemon=True) | |
| self.backup_thread.start() | |
| def get_utc_timestamp(self): | |
| """Get current UTC timestamp""" | |
| return datetime.utcnow().isoformat() + 'Z' | |
| def load_data(self): | |
| """Load data from backup file if it exists""" | |
| try: | |
| if os.path.exists(self.backup_file): | |
| with open(self.backup_file, 'r', encoding='utf-8') as f: | |
| loaded_data = json.load(f) | |
| # Migrate old data structure if needed | |
| if 'chat_messages' not in loaded_data: | |
| loaded_data['chat_messages'] = [] | |
| loaded_data['next_chat_id'] = 1 | |
| if 'app_version' not in loaded_data: | |
| loaded_data['app_version'] = '2.0.0' | |
| self.data.update(loaded_data) | |
| logger.info(f"Data loaded from {self.backup_file}: {len(self.data['posts'])} posts, {len(self.data['chat_messages'])} chat messages") | |
| else: | |
| logger.info("No backup file found, starting with empty data") | |
| except Exception as e: | |
| logger.error(f"Error loading data: {str(e)}") | |
| def save_data(self): | |
| """Save data to backup file""" | |
| try: | |
| with self.lock: | |
| self.data['last_updated'] = self.get_utc_timestamp() | |
| with open(self.backup_file, 'w', encoding='utf-8') as f: | |
| json.dump(self.data, f, ensure_ascii=False, indent=2) | |
| logger.info(f"Data saved to {self.backup_file}") | |
| except Exception as e: | |
| logger.error(f"Error saving data: {str(e)}") | |
| def periodic_backup(self): | |
| """Periodically backup data every 30 seconds""" | |
| while True: | |
| time.sleep(30) | |
| self.save_data() | |
| def add_post(self, name, note, youtube_link=''): | |
| """Add a new post with UTC timestamp""" | |
| try: | |
| with self.lock: | |
| post = { | |
| 'id': self.data['next_post_id'], | |
| 'name': str(name).strip(), | |
| 'note': str(note).strip(), | |
| 'youtube_link': str(youtube_link).strip(), | |
| 'likes': 0, | |
| 'created_at': self.get_utc_timestamp() | |
| } | |
| self.data['posts'].append(post) | |
| self.data['next_post_id'] += 1 | |
| self.save_data() | |
| logger.info(f"Post added: ID {post['id']}, Name: {post['name']}") | |
| return post | |
| except Exception as e: | |
| logger.error(f"Error adding post: {str(e)}") | |
| raise | |
| def get_posts(self): | |
| """Get all posts sorted by creation date (newest first)""" | |
| try: | |
| with self.lock: | |
| posts = sorted(self.data['posts'], key=lambda x: x['created_at'], reverse=True) | |
| logger.info(f"Retrieved {len(posts)} posts") | |
| return posts | |
| except Exception as e: | |
| logger.error(f"Error getting posts: {str(e)}") | |
| return [] | |
| def like_post(self, post_id): | |
| """Like a post""" | |
| try: | |
| with self.lock: | |
| for post in self.data['posts']: | |
| if post['id'] == post_id: | |
| post['likes'] += 1 | |
| self.save_data() | |
| logger.info(f"Post {post_id} liked, total likes: {post['likes']}") | |
| return post['likes'] | |
| logger.warning(f"Post {post_id} not found for liking") | |
| return None | |
| except Exception as e: | |
| logger.error(f"Error liking post {post_id}: {str(e)}") | |
| return None | |
| def add_comment(self, post_id, name, comment): | |
| """Add a comment to a post with UTC timestamp""" | |
| try: | |
| with self.lock: | |
| # Check if post exists | |
| post_exists = any(post['id'] == post_id for post in self.data['posts']) | |
| if not post_exists: | |
| logger.warning(f"Post {post_id} not found for commenting") | |
| return None | |
| comment_obj = { | |
| 'id': self.data['next_comment_id'], | |
| 'post_id': post_id, | |
| 'name': str(name).strip(), | |
| 'comment': str(comment).strip(), | |
| 'created_at': self.get_utc_timestamp() | |
| } | |
| self.data['comments'].append(comment_obj) | |
| self.data['next_comment_id'] += 1 | |
| self.save_data() | |
| logger.info(f"Comment added to post {post_id}") | |
| return comment_obj | |
| except Exception as e: | |
| logger.error(f"Error adding comment to post {post_id}: {str(e)}") | |
| return None | |
| def get_comments(self, post_id): | |
| """Get comments for a specific post""" | |
| try: | |
| with self.lock: | |
| comments = [c for c in self.data['comments'] if c['post_id'] == post_id] | |
| logger.info(f"Retrieved {len(comments)} comments for post {post_id}") | |
| return comments | |
| except Exception as e: | |
| logger.error(f"Error getting comments for post {post_id}: {str(e)}") | |
| return [] | |
| def add_chat_message(self, name, message): | |
| """Add a chat message with UTC timestamp""" | |
| try: | |
| with self.lock: | |
| chat_msg = { | |
| 'id': self.data['next_chat_id'], | |
| 'name': str(name).strip(), | |
| 'message': str(message).strip(), | |
| 'created_at': self.get_utc_timestamp() | |
| } | |
| self.data['chat_messages'].append(chat_msg) | |
| self.data['next_chat_id'] += 1 | |
| # Keep only last 100 chat messages to prevent memory issues | |
| if len(self.data['chat_messages']) > 100: | |
| self.data['chat_messages'] = self.data['chat_messages'][-100:] | |
| self.save_data() | |
| logger.info(f"Chat message added: ID {chat_msg['id']}, Name: {chat_msg['name']}") | |
| return chat_msg | |
| except Exception as e: | |
| logger.error(f"Error adding chat message: {str(e)}") | |
| return None | |
| def get_chat_messages(self, limit=50): | |
| """Get recent chat messages""" | |
| try: | |
| with self.lock: | |
| messages = sorted(self.data['chat_messages'], key=lambda x: x['created_at'], reverse=True)[:limit] | |
| messages.reverse() # Show oldest first | |
| logger.info(f"Retrieved {len(messages)} chat messages") | |
| return messages | |
| except Exception as e: | |
| logger.error(f"Error getting chat messages: {str(e)}") | |
| return [] | |
| def get_stats(self): | |
| """Get application statistics""" | |
| try: | |
| with self.lock: | |
| return { | |
| 'total_posts': len(self.data['posts']), | |
| 'total_comments': len(self.data['comments']), | |
| 'total_chat_messages': len(self.data['chat_messages']), | |
| 'total_likes': sum(post['likes'] for post in self.data['posts']), | |
| 'app_version': self.data.get('app_version', '2.0.0'), | |
| 'last_backup': self.get_utc_timestamp(), | |
| 'server_time': self.get_utc_timestamp() | |
| } | |
| except Exception as e: | |
| logger.error(f"Error getting stats: {str(e)}") | |
| return { | |
| 'total_posts': 0, | |
| 'total_comments': 0, | |
| 'total_chat_messages': 0, | |
| 'total_likes': 0, | |
| 'app_version': '2.0.0', | |
| 'last_backup': self.get_utc_timestamp(), | |
| 'server_time': self.get_utc_timestamp() | |
| } | |
| # Initialize enhanced data store | |
| data_store = EnhancedDataStore() | |
| # Keep-alive mechanism | |
| class KeepAlive: | |
| def __init__(self): | |
| self.last_activity = time.time() | |
| self.keep_alive_thread = threading.Thread(target=self.keep_alive_loop, daemon=True) | |
| self.keep_alive_thread.start() | |
| def update_activity(self): | |
| """Update last activity timestamp""" | |
| self.last_activity = time.time() | |
| def keep_alive_loop(self): | |
| """Keep the application active""" | |
| while True: | |
| time.sleep(300) # Every 5 minutes | |
| current_time = time.time() | |
| if current_time - self.last_activity < 3600: # If activity within last hour | |
| stats = data_store.get_stats() | |
| logger.info(f"Keep-alive: App v{stats['app_version']} is active. Stats: {stats}") | |
| time.sleep(300) | |
| # Initialize keep-alive | |
| keep_alive = KeepAlive() | |
| # Enhanced HTML Template with Chat Tab and Time Sync | |
| HTML_TEMPLATE = ''' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Share YouTube v2.0 - Enhanced with Chat & Time Sync</title> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| .container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 30px; | |
| border-radius: 15px; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| backdrop-filter: blur(10px); | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| color: #FF0000; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .header p { | |
| font-size: 1.1rem; | |
| color: #666; | |
| } | |
| .version-badge { | |
| display: inline-block; | |
| background: #28a745; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| margin-left: 10px; | |
| } | |
| .time-sync { | |
| background: rgba(0, 123, 255, 0.1); | |
| padding: 10px; | |
| border-radius: 8px; | |
| margin-top: 15px; | |
| font-size: 0.9rem; | |
| color: #007bff; | |
| } | |
| .stats-bar { | |
| display: flex; | |
| justify-content: space-around; | |
| background: rgba(255, 255, 255, 0.9); | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-top: 15px; | |
| font-size: 0.9rem; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| } | |
| .stat-number { | |
| font-weight: bold; | |
| color: #FF0000; | |
| font-size: 1.2rem; | |
| } | |
| .tabs { | |
| display: flex; | |
| background: rgba(255, 255, 255, 0.9); | |
| border-radius: 15px 15px 0 0; | |
| margin-bottom: 0; | |
| overflow: hidden; | |
| } | |
| .tab { | |
| flex: 1; | |
| padding: 15px 20px; | |
| background: rgba(255, 255, 255, 0.7); | |
| border: none; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: #666; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .tab.active { | |
| background: rgba(255, 255, 255, 1); | |
| color: #FF0000; | |
| box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); | |
| } | |
| .tab:hover { | |
| background: rgba(255, 255, 255, 0.9); | |
| } | |
| .tab-content { | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 30px; | |
| border-radius: 0 0 15px 15px; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| backdrop-filter: blur(10px); | |
| min-height: 500px; | |
| } | |
| .tab-pane { | |
| display: none; | |
| } | |
| .tab-pane.active { | |
| display: block; | |
| } | |
| .share-card h2, | |
| .chat-card h2 { | |
| color: #333; | |
| margin-bottom: 25px; | |
| font-size: 1.5rem; | |
| text-align: center; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: #555; | |
| } | |
| .form-group input, | |
| .form-group textarea { | |
| width: 100%; | |
| padding: 12px 15px; | |
| border: 2px solid #e1e5e9; | |
| border-radius: 10px; | |
| font-size: 1rem; | |
| transition: all 0.3s ease; | |
| background: rgba(255, 255, 255, 0.9); | |
| } | |
| .form-group input:focus, | |
| .form-group textarea:focus { | |
| outline: none; | |
| border-color: #FF0000; | |
| box-shadow: 0 0 0 3px rgba(255, 0, 0, 0.1); | |
| transform: translateY(-2px); | |
| } | |
| .form-group textarea { | |
| resize: vertical; | |
| min-height: 80px; | |
| } | |
| .btn-share, | |
| .btn-send { | |
| width: 100%; | |
| padding: 15px; | |
| background: linear-gradient(45deg, #FF0000, #CC0000); | |
| color: white; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .btn-send { | |
| background: linear-gradient(45deg, #007bff, #0056b3); | |
| } | |
| .btn-share:hover, | |
| .btn-send:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(255, 0, 0, 0.3); | |
| } | |
| .btn-send:hover { | |
| box-shadow: 0 8px 25px rgba(0, 123, 255, 0.3); | |
| } | |
| .btn-share:disabled, | |
| .btn-send:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .chat-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 500px; | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| background: #f8f9fa; | |
| border: 1px solid #e1e5e9; | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| overflow-y: auto; | |
| max-height: 350px; | |
| } | |
| .chat-message { | |
| background: white; | |
| padding: 10px 15px; | |
| border-radius: 10px; | |
| margin-bottom: 10px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | |
| } | |
| .chat-message-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| } | |
| .chat-message-name { | |
| font-weight: 600; | |
| color: #007bff; | |
| } | |
| .chat-message-time { | |
| font-size: 0.8rem; | |
| color: #888; | |
| } | |
| .chat-message-text { | |
| color: #555; | |
| line-height: 1.4; | |
| } | |
| .chat-form { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .chat-input { | |
| flex: 1; | |
| padding: 12px 15px; | |
| border: 2px solid #e1e5e9; | |
| border-radius: 25px; | |
| font-size: 1rem; | |
| } | |
| .chat-send-btn { | |
| padding: 12px 20px; | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| } | |
| .link-card { | |
| background: #fff; | |
| border: 1px solid #e1e5e9; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); | |
| transition: all 0.3s ease; | |
| } | |
| .link-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); | |
| } | |
| .link-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 15px; | |
| } | |
| .link-header .user-name { | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .link-header .timestamp { | |
| color: #888; | |
| font-size: 0.9rem; | |
| } | |
| .link-note { | |
| margin-bottom: 15px; | |
| color: #555; | |
| line-height: 1.5; | |
| } | |
| .youtube-embed { | |
| margin-bottom: 15px; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .youtube-embed iframe { | |
| width: 100%; | |
| height: 315px; | |
| border: none; | |
| } | |
| .link-actions { | |
| display: flex; | |
| gap: 15px; | |
| padding-top: 15px; | |
| border-top: 1px solid #e1e5e9; | |
| } | |
| .action-btn { | |
| background: none; | |
| border: none; | |
| padding: 8px 15px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .like-btn { | |
| color: #666; | |
| } | |
| .like-btn:hover, | |
| .like-btn.liked { | |
| background: rgba(255, 0, 0, 0.1); | |
| color: #FF0000; | |
| } | |
| .comment-btn { | |
| color: #666; | |
| } | |
| .comment-btn:hover { | |
| background: rgba(0, 123, 255, 0.1); | |
| color: #007bff; | |
| } | |
| .comments-section { | |
| margin-top: 15px; | |
| padding-top: 15px; | |
| border-top: 1px solid #e1e5e9; | |
| } | |
| .comment-form { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .comment-form input { | |
| flex: 1; | |
| padding: 8px 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| } | |
| .comment-form button { | |
| padding: 8px 15px; | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| } | |
| .comment-form button:hover { | |
| background: #0056b3; | |
| } | |
| .comment { | |
| background: #f8f9fa; | |
| padding: 10px 15px; | |
| border-radius: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .comment-author { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| } | |
| .comment-text { | |
| color: #555; | |
| font-size: 0.9rem; | |
| } | |
| .loading-spinner { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 30px; | |
| border-radius: 15px; | |
| text-align: center; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); | |
| display: none; | |
| z-index: 1000; | |
| } | |
| .loading-spinner i { | |
| font-size: 2rem; | |
| color: #FF0000; | |
| margin-bottom: 10px; | |
| } | |
| .success-message, | |
| .error-message { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 15px 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); | |
| display: none; | |
| align-items: center; | |
| gap: 10px; | |
| z-index: 1000; | |
| } | |
| .success-message { | |
| background: #28a745; | |
| color: white; | |
| } | |
| .error-message { | |
| background: #dc3545; | |
| color: white; | |
| } | |
| .success-message i, | |
| .error-message i { | |
| font-size: 1.2rem; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 15px; | |
| } | |
| .header h1 { | |
| font-size: 2rem; | |
| } | |
| .tab-content { | |
| padding: 20px; | |
| } | |
| .youtube-embed iframe { | |
| height: 200px; | |
| } | |
| .link-actions { | |
| flex-wrap: wrap; | |
| } | |
| .stats-bar { | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .chat-container { | |
| height: 400px; | |
| } | |
| .chat-messages { | |
| max-height: 250px; | |
| } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .link-card, | |
| .chat-message { | |
| animation: fadeInUp 0.5s ease; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-content"> | |
| <h1> | |
| <i class="fab fa-youtube"></i> Share YouTube | |
| <span class="version-badge" id="versionBadge">v2.0</span> | |
| </h1> | |
| <p>Share videos, chat with friends - Enhanced with real-time sync!</p> | |
| <div class="time-sync" id="timeSync"> | |
| <i class="fas fa-clock"></i> | |
| <span>Syncing time with server...</span> | |
| </div> | |
| <div class="stats-bar" id="statsBar"> | |
| <div class="stat-item"> | |
| <div class="stat-number" id="totalPosts">0</div> | |
| <div>Posts</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number" id="totalComments">0</div> | |
| <div>Comments</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number" id="totalLikes">0</div> | |
| <div>Likes</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number" id="totalChatMessages">0</div> | |
| <div>Chat Messages</div> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Tabs --> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('share')"> | |
| <i class="fas fa-share"></i> | |
| Share Videos | |
| </button> | |
| <button class="tab" onclick="switchTab('chat')"> | |
| <i class="fas fa-comments"></i> | |
| Live Chat | |
| </button> | |
| </div> | |
| <!-- Tab Content --> | |
| <div class="tab-content"> | |
| <!-- Share Tab --> | |
| <div id="shareTab" class="tab-pane active"> | |
| <div class="share-card"> | |
| <h2><i class="fas fa-share"></i> Share YouTube Link</h2> | |
| <form id="shareForm"> | |
| <div class="form-group"> | |
| <label for="name"><i class="fas fa-user"></i> Your Name:</label> | |
| <input type="text" id="name" name="name" required placeholder="Enter your name"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="youtube_link"><i class="fab fa-youtube"></i> YouTube Link:</label> | |
| <input type="url" id="youtube_link" name="youtube_link" required placeholder="https://www.youtube.com/watch?v=..."> | |
| </div> | |
| <div class="form-group"> | |
| <label for="note"><i class="fas fa-sticky-note"></i> Short Note:</label> | |
| <textarea id="note" name="note" required placeholder="Write a short note about this video..."></textarea> | |
| </div> | |
| <button type="submit" class="btn-share" id="shareBtn"> | |
| <i class="fas fa-share"></i> Share | |
| </button> | |
| </form> | |
| </div> | |
| <!-- Shared Links Feed --> | |
| <div style="margin-top: 30px;"> | |
| <h2 style="text-align: center; color: #333; margin-bottom: 25px;"> | |
| <i class="fas fa-list"></i> Shared Links | |
| </h2> | |
| <div id="linksContainer"> | |
| <!-- Shared links will be loaded here --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Tab --> | |
| <div id="chatTab" class="tab-pane"> | |
| <div class="chat-card"> | |
| <h2><i class="fas fa-comments"></i> Live Chat</h2> | |
| <div class="chat-container"> | |
| <div class="chat-messages" id="chatMessages"> | |
| <!-- Chat messages will be loaded here --> | |
| </div> | |
| <div class="chat-form"> | |
| <input type="text" id="chatInput" class="chat-input" placeholder="Type your message..." maxlength="500"> | |
| <button type="button" id="chatSendBtn" class="chat-send-btn"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Loading Spinner --> | |
| <div id="loadingSpinner" class="loading-spinner"> | |
| <i class="fas fa-spinner fa-spin"></i> | |
| <p>Loading...</p> | |
| </div> | |
| <!-- Success Message --> | |
| <div id="successMessage" class="success-message"> | |
| <i class="fas fa-check-circle"></i> | |
| <span id="successText">Success!</span> | |
| </div> | |
| <!-- Error Message --> | |
| <div id="errorMessage" class="error-message"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| <span id="errorText">An error occurred!</span> | |
| </div> | |
| <script> | |
| // API Base URL | |
| const API_BASE_URL = '/api'; | |
| // Global variables | |
| let serverTimeOffset = 0; | |
| let currentTab = 'share'; | |
| let chatRefreshInterval; | |
| let statsRefreshInterval; | |
| // DOM Elements | |
| const shareForm = document.getElementById('shareForm'); | |
| const shareBtn = document.getElementById('shareBtn'); | |
| const linksContainer = document.getElementById('linksContainer'); | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const chatInput = document.getElementById('chatInput'); | |
| const chatSendBtn = document.getElementById('chatSendBtn'); | |
| const loadingSpinner = document.getElementById('loadingSpinner'); | |
| const successMessage = document.getElementById('successMessage'); | |
| const successText = document.getElementById('successText'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const errorText = document.getElementById('errorText'); | |
| const timeSync = document.getElementById('timeSync'); | |
| const versionBadge = document.getElementById('versionBadge'); | |
| // Stats elements | |
| const totalPosts = document.getElementById('totalPosts'); | |
| const totalComments = document.getElementById('totalComments'); | |
| const totalLikes = document.getElementById('totalLikes'); | |
| const totalChatMessages = document.getElementById('totalChatMessages'); | |
| // Initialize app | |
| document.addEventListener('DOMContentLoaded', function() { | |
| console.log('Enhanced Share YouTube App v2.0 initialized'); | |
| // Initialize time synchronization | |
| syncTimeWithServer(); | |
| // Load initial data | |
| loadStats(); | |
| loadLinks(); | |
| loadChatMessages(); | |
| // Set up event listeners | |
| shareForm.addEventListener('submit', handleShareSubmit); | |
| chatSendBtn.addEventListener('click', handleChatSend); | |
| chatInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| handleChatSend(); | |
| } | |
| }); | |
| // Set up periodic refreshes | |
| statsRefreshInterval = setInterval(loadStats, 30000); // Every 30 seconds | |
| chatRefreshInterval = setInterval(loadChatMessages, 5000); // Every 5 seconds for chat | |
| // Sync time every 5 minutes | |
| setInterval(syncTimeWithServer, 300000); | |
| }); | |
| // Time synchronization | |
| async function syncTimeWithServer() { | |
| try { | |
| const startTime = Date.now(); | |
| const response = await fetch('/api/time'); | |
| const endTime = Date.now(); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| const serverTime = new Date(data.server_time).getTime(); | |
| const networkDelay = (endTime - startTime) / 2; | |
| const clientTime = endTime - networkDelay; | |
| serverTimeOffset = serverTime - clientTime; | |
| const localTime = new Date().toLocaleString(); | |
| const syncedTime = new Date(Date.now() + serverTimeOffset).toLocaleString(); | |
| timeSync.innerHTML = ` | |
| <i class="fas fa-clock"></i> | |
| Time synced! Local: ${localTime} | Server: ${syncedTime} | |
| `; | |
| console.log('Time synchronized. Offset:', serverTimeOffset, 'ms'); | |
| } | |
| } catch (error) { | |
| console.error('Time sync failed:', error); | |
| timeSync.innerHTML = ` | |
| <i class="fas fa-exclamation-triangle"></i> | |
| Time sync failed - using local time | |
| `; | |
| } | |
| } | |
| // Get synchronized time | |
| function getSyncedTime() { | |
| return new Date(Date.now() + serverTimeOffset); | |
| } | |
| // Tab switching | |
| function switchTab(tabName) { | |
| // Update tab buttons | |
| document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| // Update tab content | |
| document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active')); | |
| document.getElementById(tabName + 'Tab').classList.add('active'); | |
| currentTab = tabName; | |
| // Load data for the active tab | |
| if (tabName === 'chat') { | |
| loadChatMessages(); | |
| // Refresh chat more frequently when viewing | |
| clearInterval(chatRefreshInterval); | |
| chatRefreshInterval = setInterval(loadChatMessages, 3000); | |
| } else { | |
| // Slower refresh when not viewing chat | |
| clearInterval(chatRefreshInterval); | |
| chatRefreshInterval = setInterval(loadChatMessages, 10000); | |
| } | |
| } | |
| // Load statistics | |
| async function loadStats() { | |
| try { | |
| const response = await fetch('/api/stats'); | |
| if (response.ok) { | |
| const stats = await response.json(); | |
| totalPosts.textContent = stats.total_posts; | |
| totalComments.textContent = stats.total_comments; | |
| totalLikes.textContent = stats.total_likes; | |
| totalChatMessages.textContent = stats.total_chat_messages; | |
| versionBadge.textContent = 'v' + stats.app_version; | |
| console.log('Stats loaded:', stats); | |
| } | |
| } catch (error) { | |
| console.error('Error loading stats:', error); | |
| } | |
| } | |
| // Handle share form submission | |
| async function handleShareSubmit(e) { | |
| e.preventDefault(); | |
| console.log('Form submitted'); | |
| const formData = new FormData(shareForm); | |
| const data = { | |
| name: formData.get('name').trim(), | |
| note: formData.get('note').trim(), | |
| youtube_link: formData.get('youtube_link').trim() | |
| }; | |
| console.log('Form data:', data); | |
| // Validate inputs | |
| if (!data.name || !data.note || !data.youtube_link) { | |
| showErrorMessage('Please fill in all fields'); | |
| return; | |
| } | |
| // Validate YouTube URL | |
| if (!isValidYouTubeURL(data.youtube_link)) { | |
| showErrorMessage('Please enter a valid YouTube URL'); | |
| return; | |
| } | |
| try { | |
| showLoading(true); | |
| shareBtn.disabled = true; | |
| shareBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sharing...'; | |
| const response = await fetch(`${API_BASE_URL}/posts`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.error || `HTTP ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| console.log('Post created:', result); | |
| showSuccessMessage('Shared successfully!'); | |
| shareForm.reset(); | |
| await Promise.all([loadLinks(), loadStats()]); | |
| } catch (error) { | |
| console.error('Error sharing link:', error); | |
| showErrorMessage('Error sharing link: ' + error.message); | |
| } finally { | |
| showLoading(false); | |
| shareBtn.disabled = false; | |
| shareBtn.innerHTML = '<i class="fas fa-share"></i> Share'; | |
| } | |
| } | |
| // Handle chat message send | |
| async function handleChatSend() { | |
| const message = chatInput.value.trim(); | |
| if (!message) { | |
| showErrorMessage('Please enter a message'); | |
| return; | |
| } | |
| const name = prompt('Please enter your name:'); | |
| if (!name || !name.trim()) return; | |
| try { | |
| chatSendBtn.disabled = true; | |
| chatSendBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; | |
| const response = await fetch(`${API_BASE_URL}/chat`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| name: name.trim(), | |
| message: message | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.error || `HTTP ${response.status}`); | |
| } | |
| chatInput.value = ''; | |
| await Promise.all([loadChatMessages(), loadStats()]); | |
| // Scroll to bottom of chat | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } catch (error) { | |
| console.error('Error sending chat message:', error); | |
| showErrorMessage('Error sending message: ' + error.message); | |
| } finally { | |
| chatSendBtn.disabled = false; | |
| chatSendBtn.innerHTML = '<i class="fas fa-paper-plane"></i>'; | |
| } | |
| } | |
| // Load chat messages | |
| async function loadChatMessages() { | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/chat`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const messages = await response.json(); | |
| displayChatMessages(messages); | |
| } catch (error) { | |
| console.error('Error loading chat messages:', error); | |
| if (currentTab === 'chat') { | |
| chatMessages.innerHTML = ` | |
| <div style="text-align: center; color: #666; padding: 20px;"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| <p>Failed to load chat messages</p> | |
| <button onclick="loadChatMessages()" style="margin-top: 10px; padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;"> | |
| Retry | |
| </button> | |
| </div> | |
| `; | |
| } | |
| } | |
| } | |
| // Display chat messages | |
| function displayChatMessages(messages) { | |
| if (messages.length === 0) { | |
| chatMessages.innerHTML = ` | |
| <div style="text-align: center; color: #666; padding: 40px;"> | |
| <i class="fas fa-comments" style="font-size: 3rem; margin-bottom: 15px;"></i> | |
| <p>No messages yet</p> | |
| <p>Be the first to start the conversation!</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| const shouldScrollToBottom = chatMessages.scrollTop + chatMessages.clientHeight >= chatMessages.scrollHeight - 10; | |
| chatMessages.innerHTML = messages.map(msg => ` | |
| <div class="chat-message"> | |
| <div class="chat-message-header"> | |
| <span class="chat-message-name"> | |
| <i class="fas fa-user"></i> ${escapeHtml(msg.name)} | |
| </span> | |
| <span class="chat-message-time">${formatTime(msg.created_at)}</span> | |
| </div> | |
| <div class="chat-message-text">${escapeHtml(msg.message)}</div> | |
| </div> | |
| `).join(''); | |
| if (shouldScrollToBottom) { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| } | |
| // Load all shared links | |
| async function loadLinks() { | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/posts`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const links = await response.json(); | |
| displayLinks(links); | |
| } catch (error) { | |
| console.error('Error loading links:', error); | |
| linksContainer.innerHTML = ` | |
| <div style="text-align: center; color: #666; padding: 20px;"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| <p>Failed to load links: ${error.message}</p> | |
| <button onclick="loadLinks()" style="margin-top: 10px; padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;"> | |
| Retry | |
| </button> | |
| </div> | |
| `; | |
| } | |
| } | |
| // Display links in the container | |
| function displayLinks(links) { | |
| if (links.length === 0) { | |
| linksContainer.innerHTML = ` | |
| <div style="text-align: center; color: #666; padding: 40px;"> | |
| <i class="fab fa-youtube" style="font-size: 3rem; margin-bottom: 15px;"></i> | |
| <p>No shared links yet</p> | |
| <p>Be the first to share a YouTube link!</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| linksContainer.innerHTML = links.map(link => createLinkCard(link)).join(''); | |
| addLinkEventListeners(); | |
| } | |
| // Create HTML for a single link card | |
| function createLinkCard(link) { | |
| const videoId = extractYouTubeVideoId(link.youtube_link); | |
| const embedUrl = videoId ? `https://www.youtube.com/embed/${videoId}` : ''; | |
| const timeAgo = formatTime(link.created_at); | |
| return ` | |
| <div class="link-card" data-link-id="${link.id}"> | |
| <div class="link-header"> | |
| <span class="user-name"> | |
| <i class="fas fa-user"></i> ${escapeHtml(link.name)} | |
| </span> | |
| <span class="timestamp">${timeAgo}</span> | |
| </div> | |
| <div class="link-note"> | |
| ${escapeHtml(link.note)} | |
| </div> | |
| ${embedUrl ? ` | |
| <div class="youtube-embed"> | |
| <iframe src="${embedUrl}" | |
| title="YouTube video player" | |
| frameborder="0" | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | |
| allowfullscreen> | |
| </iframe> | |
| </div> | |
| ` : ` | |
| <div class="youtube-link"> | |
| <a href="${link.youtube_link}" target="_blank" rel="noopener noreferrer"> | |
| <i class="fab fa-youtube"></i> Watch on YouTube | |
| </a> | |
| </div> | |
| `} | |
| <div class="link-actions"> | |
| <button class="action-btn like-btn" data-link-id="${link.id}"> | |
| <i class="fas fa-heart"></i> | |
| <span class="like-count">${link.likes}</span> | |
| </button> | |
| <button class="action-btn comment-btn" data-link-id="${link.id}"> | |
| <i class="fas fa-comment"></i> | |
| Comment | |
| </button> | |
| </div> | |
| <div class="comments-section" id="comments-${link.id}" style="display: none;"> | |
| <div class="comment-form"> | |
| <input type="text" placeholder="Write a comment..." class="comment-input"> | |
| <button type="button" class="add-comment-btn" data-link-id="${link.id}"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| <div class="comments-list" id="comments-list-${link.id}"> | |
| <!-- Comments will be loaded here --> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Add event listeners for link interactions | |
| function addLinkEventListeners() { | |
| document.querySelectorAll('.like-btn').forEach(btn => { | |
| btn.addEventListener('click', handleLike); | |
| }); | |
| document.querySelectorAll('.comment-btn').forEach(btn => { | |
| btn.addEventListener('click', toggleComments); | |
| }); | |
| document.querySelectorAll('.add-comment-btn').forEach(btn => { | |
| btn.addEventListener('click', handleAddComment); | |
| }); | |
| document.querySelectorAll('.comment-input').forEach(input => { | |
| input.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| const linkId = this.closest('.comments-section').id.split('-')[1]; | |
| const btn = document.querySelector(`.add-comment-btn[data-link-id="${linkId}"]`); | |
| btn.click(); | |
| } | |
| }); | |
| }); | |
| } | |
| // Handle like button click | |
| async function handleLike(e) { | |
| const linkId = e.currentTarget.dataset.linkId; | |
| const likeBtn = e.currentTarget; | |
| const likeCount = likeBtn.querySelector('.like-count'); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/posts/${linkId}/like`, { | |
| method: 'POST' | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| likeCount.textContent = result.likes; | |
| likeBtn.classList.add('liked'); | |
| setTimeout(() => likeBtn.classList.remove('liked'), 1000); | |
| loadStats(); | |
| } catch (error) { | |
| console.error('Error liking post:', error); | |
| showErrorMessage('Error liking post'); | |
| } | |
| } | |
| // Toggle comments section | |
| async function toggleComments(e) { | |
| const linkId = e.currentTarget.dataset.linkId; | |
| const commentsSection = document.getElementById(`comments-${linkId}`); | |
| if (commentsSection.style.display === 'none') { | |
| commentsSection.style.display = 'block'; | |
| await loadComments(linkId); | |
| } else { | |
| commentsSection.style.display = 'none'; | |
| } | |
| } | |
| // Load comments for a specific link | |
| async function loadComments(linkId) { | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/posts/${linkId}/comments`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const comments = await response.json(); | |
| const commentsList = document.getElementById(`comments-list-${linkId}`); | |
| if (comments.length === 0) { | |
| commentsList.innerHTML = '<p style="text-align: center; color: #666; padding: 10px;">No comments yet</p>'; | |
| } else { | |
| commentsList.innerHTML = comments.map(comment => ` | |
| <div class="comment"> | |
| <div class="comment-author"> | |
| <i class="fas fa-user"></i> ${escapeHtml(comment.name)} | |
| <span style="color: #888; font-size: 0.8rem; margin-left: 10px;">${formatTime(comment.created_at)}</span> | |
| </div> | |
| <div class="comment-text">${escapeHtml(comment.comment)}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| } catch (error) { | |
| console.error('Error loading comments:', error); | |
| } | |
| } | |
| // Handle add comment | |
| async function handleAddComment(e) { | |
| const linkId = e.currentTarget.dataset.linkId; | |
| const commentsSection = document.getElementById(`comments-${linkId}`); | |
| const commentInput = commentsSection.querySelector('.comment-input'); | |
| const commentText = commentInput.value.trim(); | |
| if (!commentText) { | |
| showErrorMessage('Please write a comment'); | |
| return; | |
| } | |
| const name = prompt('Please enter your name:'); | |
| if (!name || !name.trim()) return; | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/posts/${linkId}/comments`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| name: name.trim(), | |
| comment: commentText | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| commentInput.value = ''; | |
| await Promise.all([loadComments(linkId), loadStats()]); | |
| showSuccessMessage('Comment added successfully!'); | |
| } catch (error) { | |
| console.error('Error adding comment:', error); | |
| showErrorMessage('Error adding comment'); | |
| } | |
| } | |
| // Utility functions | |
| function isValidYouTubeURL(url) { | |
| const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)[a-zA-Z0-9_-]{11}/; | |
| return youtubeRegex.test(url); | |
| } | |
| function extractYouTubeVideoId(url) { | |
| const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/; | |
| const match = url.match(regex); | |
| return match ? match[1] : null; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function formatTime(utcTimeString) { | |
| try { | |
| const utcTime = new Date(utcTimeString); | |
| const now = getSyncedTime(); | |
| const diffInSeconds = Math.floor((now - utcTime) / 1000); | |
| if (diffInSeconds < 60) return 'just now'; | |
| if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`; | |
| if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`; | |
| if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`; | |
| // For older posts, show the actual date | |
| return utcTime.toLocaleDateString() + ' ' + utcTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| } catch (error) { | |
| console.error('Error formatting time:', error); | |
| return 'unknown time'; | |
| } | |
| } | |
| function showLoading(show) { | |
| loadingSpinner.style.display = show ? 'block' : 'none'; | |
| } | |
| function showSuccessMessage(message) { | |
| successText.textContent = message; | |
| successMessage.style.display = 'flex'; | |
| setTimeout(() => { | |
| successMessage.style.display = 'none'; | |
| }, 3000); | |
| } | |
| function showErrorMessage(message) { | |
| errorText.textContent = message; | |
| errorMessage.style.display = 'flex'; | |
| setTimeout(() => { | |
| errorMessage.style.display = 'none'; | |
| }, 5000); | |
| } | |
| // Cleanup intervals when page unloads | |
| window.addEventListener('beforeunload', function() { | |
| clearInterval(statsRefreshInterval); | |
| clearInterval(chatRefreshInterval); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| # Enhanced API Routes | |
| def get_server_time(): | |
| """Get server time for synchronization""" | |
| try: | |
| keep_alive.update_activity() | |
| return jsonify({ | |
| 'server_time': data_store.get_utc_timestamp(), | |
| 'timezone': 'UTC' | |
| }) | |
| except Exception as e: | |
| logger.error(f"Error getting server time: {str(e)}") | |
| return jsonify({'error': 'Failed to get server time'}), 500 | |
| def get_posts(): | |
| """Get all posts""" | |
| try: | |
| keep_alive.update_activity() | |
| posts = data_store.get_posts() | |
| logger.info(f"API: Returning {len(posts)} posts") | |
| return jsonify(posts) | |
| except Exception as e: | |
| logger.error(f"API Error getting posts: {str(e)}") | |
| return jsonify({'error': 'Failed to get posts'}), 500 | |
| def create_post(): | |
| """Create a new post""" | |
| try: | |
| keep_alive.update_activity() | |
| data = request.get_json() | |
| logger.info(f"API: Received post data: {data}") | |
| if not data: | |
| logger.warning("API: No JSON data received") | |
| return jsonify({'error': 'No data provided'}), 400 | |
| name = data.get('name', '').strip() | |
| note = data.get('note', '').strip() | |
| youtube_link = data.get('youtube_link', '').strip() | |
| if not name or not note: | |
| logger.warning(f"API: Missing required fields - name: {bool(name)}, note: {bool(note)}") | |
| return jsonify({'error': 'Name and note are required'}), 400 | |
| post = data_store.add_post(name=name, note=note, youtube_link=youtube_link) | |
| logger.info(f"API: Post created successfully: {post['id']}") | |
| return jsonify(post), 201 | |
| except Exception as e: | |
| logger.error(f"API Error creating post: {str(e)}") | |
| return jsonify({'error': f'Failed to create post: {str(e)}'}), 500 | |
| def like_post(post_id): | |
| """Like a post""" | |
| try: | |
| keep_alive.update_activity() | |
| likes = data_store.like_post(post_id) | |
| if likes is None: | |
| logger.warning(f"API: Post {post_id} not found for liking") | |
| return jsonify({'error': 'Post not found'}), 404 | |
| logger.info(f"API: Post {post_id} liked, total likes: {likes}") | |
| return jsonify({'likes': likes}) | |
| except Exception as e: | |
| logger.error(f"API Error liking post {post_id}: {str(e)}") | |
| return jsonify({'error': 'Failed to like post'}), 500 | |
| def get_comments(post_id): | |
| """Get comments for a post""" | |
| try: | |
| keep_alive.update_activity() | |
| comments = data_store.get_comments(post_id) | |
| logger.info(f"API: Returning {len(comments)} comments for post {post_id}") | |
| return jsonify(comments) | |
| except Exception as e: | |
| logger.error(f"API Error getting comments for post {post_id}: {str(e)}") | |
| return jsonify({'error': 'Failed to get comments'}), 500 | |
| def add_comment(post_id): | |
| """Add a comment to a post""" | |
| try: | |
| keep_alive.update_activity() | |
| data = request.get_json() | |
| logger.info(f"API: Received comment data for post {post_id}: {data}") | |
| if not data: | |
| return jsonify({'error': 'No data provided'}), 400 | |
| name = data.get('name', '').strip() | |
| comment_text = data.get('comment', '').strip() | |
| if not name or not comment_text: | |
| logger.warning(f"API: Missing required fields - name: {bool(name)}, comment: {bool(comment_text)}") | |
| return jsonify({'error': 'Name and comment are required'}), 400 | |
| comment = data_store.add_comment(post_id=post_id, name=name, comment=comment_text) | |
| if comment is None: | |
| logger.warning(f"API: Post {post_id} not found for commenting") | |
| return jsonify({'error': 'Post not found'}), 404 | |
| logger.info(f"API: Comment added to post {post_id}") | |
| return jsonify(comment), 201 | |
| except Exception as e: | |
| logger.error(f"API Error adding comment to post {post_id}: {str(e)}") | |
| return jsonify({'error': 'Failed to add comment'}), 500 | |
| def get_chat_messages(): | |
| """Get chat messages""" | |
| try: | |
| keep_alive.update_activity() | |
| messages = data_store.get_chat_messages() | |
| logger.info(f"API: Returning {len(messages)} chat messages") | |
| return jsonify(messages) | |
| except Exception as e: | |
| logger.error(f"API Error getting chat messages: {str(e)}") | |
| return jsonify({'error': 'Failed to get chat messages'}), 500 | |
| def add_chat_message(): | |
| """Add a chat message""" | |
| try: | |
| keep_alive.update_activity() | |
| data = request.get_json() | |
| logger.info(f"API: Received chat message data: {data}") | |
| if not data: | |
| return jsonify({'error': 'No data provided'}), 400 | |
| name = data.get('name', '').strip() | |
| message = data.get('message', '').strip() | |
| if not name or not message: | |
| logger.warning(f"API: Missing required fields - name: {bool(name)}, message: {bool(message)}") | |
| return jsonify({'error': 'Name and message are required'}), 400 | |
| chat_msg = data_store.add_chat_message(name=name, message=message) | |
| if chat_msg is None: | |
| return jsonify({'error': 'Failed to add chat message'}), 500 | |
| logger.info(f"API: Chat message added: {chat_msg['id']}") | |
| return jsonify(chat_msg), 201 | |
| except Exception as e: | |
| logger.error(f"API Error adding chat message: {str(e)}") | |
| return jsonify({'error': 'Failed to add chat message'}), 500 | |
| def get_stats(): | |
| """Get application statistics""" | |
| try: | |
| keep_alive.update_activity() | |
| stats = data_store.get_stats() | |
| logger.info(f"API: Returning stats: {stats}") | |
| return jsonify(stats) | |
| except Exception as e: | |
| logger.error(f"API Error getting stats: {str(e)}") | |
| return jsonify({'error': 'Failed to get stats'}), 500 | |
| # Health check endpoint | |
| def health_check(): | |
| """Health check endpoint""" | |
| try: | |
| keep_alive.update_activity() | |
| stats = data_store.get_stats() | |
| return jsonify({ | |
| 'status': 'healthy', | |
| 'timestamp': data_store.get_utc_timestamp(), | |
| 'stats': stats | |
| }) | |
| except Exception as e: | |
| logger.error(f"Health check error: {str(e)}") | |
| return jsonify({'status': 'unhealthy', 'error': str(e)}), 500 | |
| # Main route | |
| def index(): | |
| keep_alive.update_activity() | |
| return render_template_string(HTML_TEMPLATE) | |
| # Error handlers | |
| def not_found(error): | |
| logger.warning(f"404 error: {request.url}") | |
| return jsonify({'error': 'Not found'}), 404 | |
| def internal_error(error): | |
| logger.error(f"500 error: {str(error)}") | |
| return jsonify({'error': 'Internal server error'}), 500 | |
| if __name__ == '__main__': | |
| port = int(os.environ.get('PORT', 7860)) | |
| logger.info(f"===== Enhanced Share YouTube App v2.0 Startup at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} =====") | |
| logger.info(f"Starting enhanced server with chat and time sync on port {port}") | |
| logger.info(f"Data will be saved to: {data_store.backup_file}") | |
| # Load initial data and show stats | |
| initial_stats = data_store.get_stats() | |
| logger.info(f"Initial stats: {initial_stats}") | |
| # Run with threading enabled and proper error handling | |
| run_simple( | |
| hostname='0.0.0.0', | |
| port=port, | |
| application=app, | |
| use_reloader=False, | |
| use_debugger=False, | |
| threaded=True, | |
| processes=1 | |
| ) | |