Share-YouTube / app.py
Taf2023's picture
Upload app.py
563eac9 verified
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
@app.route('/api/time', methods=['GET'])
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
@app.route('/api/posts', methods=['GET'])
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
@app.route('/api/posts', methods=['POST'])
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
@app.route('/api/posts/<int:post_id>/like', methods=['POST'])
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
@app.route('/api/posts/<int:post_id>/comments', methods=['GET'])
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
@app.route('/api/posts/<int:post_id>/comments', methods=['POST'])
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
@app.route('/api/chat', methods=['GET'])
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
@app.route('/api/chat', methods=['POST'])
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
@app.route('/api/stats', methods=['GET'])
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
@app.route('/health')
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
@app.route('/')
def index():
keep_alive.update_activity()
return render_template_string(HTML_TEMPLATE)
# Error handlers
@app.errorhandler(404)
def not_found(error):
logger.warning(f"404 error: {request.url}")
return jsonify({'error': 'Not found'}), 404
@app.errorhandler(500)
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
)