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 | |
) | |