diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -18,12 +18,15 @@ import base64 from functools import wraps from contextlib import contextmanager -# إنشاء المجلدات الضرورية +# استيراد Cloudinary +import cloudinary +import cloudinary.uploader +import cloudinary.api + +# إنشاء المجلدات الضرورية (للصور الشخصية فقط) os.makedirs('logs', exist_ok=True) -os.makedirs('temp_videos', exist_ok=True) os.makedirs('avatars', exist_ok=True) os.makedirs('thumbnails', exist_ok=True) -os.makedirs('affiliate_logs', exist_ok=True) # إعداد التسجيل logging.basicConfig( @@ -33,35 +36,20 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -# محاولة استيراد المكتبات +# محاولة استيراد المكتبات مع fallback try: from PIL import Image, ImageDraw, ImageFont HAS_PIL = True except ImportError: HAS_PIL = False - logger.warning("PIL غير مثبت") + logger.warning("PIL غير مثبت، ميزات الصور محدودة.") try: import cv2 HAS_CV2 = True except ImportError: HAS_CV2 = False - logger.warning("OpenCV غير مثبت") - -try: - import cloudinary - import cloudinary.uploader - import cloudinary.api - CLOUDINARY_CONFIG = { - 'cloud_name': 'dpylnwrw0', - 'api_key': '631276857136451', - 'api_secret': 'xpehguQcV_7nj0iBXsMNM5PssHE' - } - cloudinary.config(**CLOUDINARY_CONFIG) - HAS_CLOUDINARY = True -except ImportError: - HAS_CLOUDINARY = False - logger.warning("Cloudinary غير مثبت") + logger.warning("OpenCV غير مثبت، ميزات كشف NSFW وتحليل الفيديو غير متاحة.") from flask import ( Flask, request, session, g, jsonify, render_template_string, @@ -70,23 +58,26 @@ from flask import ( from werkzeug.utils import secure_filename from werkzeug.security import generate_password_hash, check_password_hash -# تهيئة التطبيق +# ---------- تكوين Cloudinary ---------- +cloudinary.config( + cloud_name="dpylnwrw0", + api_key="631276857136451", + api_secret="xpehguQcV_7nj0iBXsMNM5PssHE" +) + +# ---------- التهيئة والإعدادات ---------- app = Flask(__name__) app.secret_key = secrets.token_hex(32) app.config['SESSION_TYPE'] = 'filesystem' app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) app.config['JSON_AS_ASCII'] = False -# إعدادات الملفات -UPLOAD_FOLDER = os.path.join(os.getcwd(), 'temp_videos') +# إعدادات رفع الملفات (للصور الشخصية فقط) AVATAR_FOLDER = os.path.join(os.getcwd(), 'avatars') THUMBNAIL_FOLDER = os.path.join(os.getcwd(), 'thumbnails') -AFFILIATE_LOG_FOLDER = os.path.join(os.getcwd(), 'affiliate_logs') ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm', '3gp'} ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} -ALLOWED_PLUGIN_EXTENSIONS = {'py'} -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['AVATAR_FOLDER'] = AVATAR_FOLDER app.config['THUMBNAIL_FOLDER'] = THUMBNAIL_FOLDER app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 @@ -95,9 +86,8 @@ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 DATABASE = os.path.join(os.getcwd(), 'arc_video.db') VECTOR_DIM = 10 notification_queues = {} -developer_plugins = {} -# دوال قاعدة البيانات +# ---------- دوال قاعدة البيانات ---------- def get_db(): db = getattr(g, '_database', None) if db is None: @@ -166,7 +156,7 @@ def init_db(): ) ''') - # جدول المتابعات + # جدول المتابعات (الأصدقاء) cursor.execute(''' CREATE TABLE IF NOT EXISTS follows ( user_id INTEGER, @@ -178,14 +168,14 @@ def init_db(): ) ''') - # جدول الفيديوهات + # جدول الفيديوهات مع Cloudinary cursor.execute(''' CREATE TABLE IF NOT EXISTS videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, cloudinary_url TEXT NOT NULL, - public_id TEXT NOT NULL, - thumbnail TEXT, + cloudinary_public_id TEXT, + thumbnail_url TEXT, title TEXT, description TEXT, duration INTEGER DEFAULT 0, @@ -207,7 +197,8 @@ def init_db(): nsfw_score REAL DEFAULT 0.0, ai_tags TEXT, processed_for_ai BOOLEAN DEFAULT 0, - FOREIGN KEY(user_id) REFERENCES users(id) + FOREIGN KEY(user_id) REFERENCES users(id), + FOREIGN KEY(challenge_id) REFERENCES challenges(id) ) ''') @@ -223,6 +214,7 @@ def init_db(): saved BOOLEAN DEFAULT 0, reported BOOLEAN DEFAULT 0, watch_time INTEGER DEFAULT 0, + voted_challenge BOOLEAN DEFAULT 0, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(video_id) REFERENCES videos(id), @@ -300,49 +292,6 @@ def init_db(): ) ''') - # جدول الرسائل الخاصة - cursor.execute(''' - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - sender_id INTEGER NOT NULL, - receiver_id INTEGER NOT NULL, - message TEXT NOT NULL, - is_read BOOLEAN DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(sender_id) REFERENCES users(id), - FOREIGN KEY(receiver_id) REFERENCES users(id) - ) - ''') - - # جدول التحديات - cursor.execute(''' - CREATE TABLE IF NOT EXISTS challenges ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - description TEXT, - prize_coins INTEGER DEFAULT 50, - start_date DATE, - end_date DATE, - is_active BOOLEAN DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # جدول مشاركات التحديات - cursor.execute(''' - CREATE TABLE IF NOT EXISTS challenge_participants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - challenge_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - video_id INTEGER NOT NULL, - votes INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(challenge_id) REFERENCES challenges(id), - FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(video_id) REFERENCES videos(id) - ) - ''') - # جدول الإحالات cursor.execute(''' CREATE TABLE IF NOT EXISTS referrals ( @@ -357,64 +306,6 @@ def init_db(): ) ''') - # جدول نقرات الإحالة - cursor.execute(''' - CREATE TABLE IF NOT EXISTS affiliate_clicks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - referrer_id INTEGER NOT NULL, - ip TEXT, - user_agent TEXT, - clicked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - converted BOOLEAN DEFAULT 0, - FOREIGN KEY(referrer_id) REFERENCES users(id) - ) - ''') - - # جدول المعاملات - cursor.execute(''' - CREATE TABLE IF NOT EXISTS transactions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - type TEXT NOT NULL, - amount INTEGER NOT NULL, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(user_id) REFERENCES users(id) - ) - ''') - - # جدول البلاغات - cursor.execute(''' - CREATE TABLE IF NOT EXISTS reports ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - reporter_id INTEGER NOT NULL, - reported_user_id INTEGER, - reported_video_id INTEGER, - reported_comment_id INTEGER, - reason TEXT NOT NULL, - status TEXT DEFAULT 'pending', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(reporter_id) REFERENCES users(id), - FOREIGN KEY(reported_user_id) REFERENCES users(id), - FOREIGN KEY(reported_video_id) REFERENCES videos(id), - FOREIGN KEY(reported_comment_id) REFERENCES comments(id) - ) - ''') - - # جدول مشاهدات الفيديو - cursor.execute(''' - CREATE TABLE IF NOT EXISTS video_views ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - video_id INTEGER NOT NULL, - watch_time INTEGER DEFAULT 0, - completed BOOLEAN DEFAULT 0, - viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(video_id) REFERENCES videos(id) - ) - ''') - # جدول نقاط الخبرة cursor.execute(''' CREATE TABLE IF NOT EXISTS user_xp ( @@ -440,52 +331,31 @@ def init_db(): ) ''') - # جدول إضافات المطورين + # جدول التحديات (مختصر) cursor.execute(''' - CREATE TABLE IF NOT EXISTS developer_plugins ( + CREATE TABLE IF NOT EXISTS challenges ( id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - name TEXT NOT NULL, - filename TEXT NOT NULL, - code TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + prize_coins INTEGER DEFAULT 50, + start_date DATE, + end_date DATE, is_active BOOLEAN DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, name) - ) - ''') - - # جدول جلسات المطورين - cursor.execute(''' - CREATE TABLE IF NOT EXISTS developer_sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - session_token TEXT UNIQUE NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL, - FOREIGN KEY(user_id) REFERENCES users(id) - ) - ''') - - # جدول أحداث التحليلات - cursor.execute(''' - CREATE TABLE IF NOT EXISTS analytics_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - event_name TEXT NOT NULL, - event_data TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') - # جدول الفيديوهات المحفوظة cursor.execute(''' - CREATE TABLE IF NOT EXISTS saved_videos ( + CREATE TABLE IF NOT EXISTS challenge_participants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + challenge_id INTEGER NOT NULL, user_id INTEGER NOT NULL, video_id INTEGER NOT NULL, - saved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + votes INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(challenge_id) REFERENCES challenges(id), FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(video_id) REFERENCES videos(id), - PRIMARY KEY (user_id, video_id) + FOREIGN KEY(video_id) REFERENCES videos(id) ) ''') @@ -494,9 +364,8 @@ def init_db(): # إنشاء المستخدمين الافتراضيين create_default_users() create_default_avatars() - create_default_challenges() -# دوال مساعدة +# ---------- دوال مساعدة ---------- def generate_referral_code(): return secrets.token_hex(4).upper() @@ -509,34 +378,26 @@ def bytes_to_vector(b): return np.zeros(VECTOR_DIM, dtype=np.float32) return np.frombuffer(b, dtype=np.float32) -def allowed_file(filename, allowed_set): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_set - def allowed_video(filename): - return allowed_file(filename, ALLOWED_VIDEO_EXTENSIONS) + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_VIDEO_EXTENSIONS def allowed_image(filename): - return allowed_file(filename, ALLOWED_IMAGE_EXTENSIONS) - -def allowed_plugin(filename): - return allowed_file(filename, ALLOWED_PLUGIN_EXTENSIONS) + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS def create_default_users(): db = get_db() - # مستخدم admin admin = db.execute("SELECT * FROM users WHERE username = 'admin'").fetchone() if not admin: hashed = generate_password_hash('admin123') referral = generate_referral_code() db.execute(''' - INSERT INTO users (username, email, password_hash, is_admin, referral_code, is_developer) - VALUES (?, ?, ?, ?, ?, ?) - ''', ('admin', 'admin@arc.com', hashed, 1, referral, 1)) + INSERT INTO users (username, email, password_hash, is_admin, referral_code) + VALUES (?, ?, ?, ?, ?) + ''', ('admin', 'admin@arc.com', hashed, 1, referral)) admin_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] db.execute('INSERT INTO user_settings (user_id) VALUES (?)', (admin_id,)) db.commit() - # مستخدم test test = db.execute("SELECT * FROM users WHERE username = 'test'").fetchone() if not test: hashed = generate_password_hash('test123') @@ -550,76 +411,77 @@ def create_default_users(): db.commit() def create_default_avatars(): + # إنشاء الصور الافتراضية إذا لم تكن موجودة (اختصار) default_avatar_path = os.path.join(AVATAR_FOLDER, 'default.jpg') default_cover_path = os.path.join(AVATAR_FOLDER, 'default_cover.jpg') - - if os.path.exists(default_avatar_path) and os.path.exists(default_cover_path): - return - - if HAS_PIL: - try: - # صورة افتراضية للبروفايل + if not os.path.exists(default_avatar_path): + if HAS_PIL: img = Image.new('RGB', (200, 200), color=(255, 45, 85)) d = ImageDraw.Draw(img) d.ellipse((20, 20, 180, 180), fill=(255, 255, 255)) d.text((70, 90), "ARC", fill=(255, 45, 85)) img.save(default_avatar_path) - - # صورة غلاف افتراضية + else: + with open(default_avatar_path, 'w') as f: + f.write('default avatar placeholder') + if not os.path.exists(default_cover_path): + if HAS_PIL: cover = Image.new('RGB', (800, 200), color=(30, 30, 30)) d = ImageDraw.Draw(cover) d.text((350, 80), "ARC VIDEO", fill=(255, 45, 85)) cover.save(default_cover_path) - except: - with open(default_avatar_path, 'w') as f: - f.write('default avatar placeholder') + else: with open(default_cover_path, 'w') as f: f.write('default cover placeholder') - else: - with open(default_avatar_path, 'w') as f: - f.write('default avatar placeholder') - with open(default_cover_path, 'w') as f: - f.write('default cover placeholder') -def create_default_challenges(): - db = get_db() - challenges = db.execute("SELECT * FROM challenges").fetchall() - if not challenges: - today = datetime.now().strftime('%Y-%m-%d') - next_week = (datetime.now() + timedelta(days=7)).strftime('%Y-%m-%d') - challenges_data = [ - ('تحدي الرقص', 'ارقص أفضل رقصة وإربح 100 عملة', 100, today, next_week), - ('تحدي الطبخ', 'اطبخ ألذ طبق', 150, today, next_week), - ('تحدي الضحك', 'أضحكنا من قلبك', 50, today, next_week), - ('تحدي التقليد', 'قلد أي شخصية مشهورة', 200, today, next_week), - ('تحدي المواهب', 'أظهر موهبتك الفريدة', 300, today, next_week) +# ---------- دوال Cloudinary ---------- +def upload_video_to_cloudinary(file_path, public_id=None): + """رفع فيديو إلى Cloudinary وإرجاع الرابط و public_id""" + try: + response = cloudinary.uploader.upload( + file_path, + resource_type="video", + public_id=public_id, + folder="arc_videos", + eager=[ + {"width": 300, "height": 300, "crop": "pad", "audio_codec": "none"}, + {"width": 160, "height": 100, "crop": "crop", "gravity": "south", "audio_codec": "none"} + ], + eager_async=True + ) + return response.get('secure_url'), response.get('public_id') + except Exception as e: + logger.error(f"Cloudinary upload error: {e}") + return None, None + +def generate_video_thumbnail(video_public_id): + """توليد رابط الصورة المصغرة من Cloudinary""" + return cloudinary.CloudinaryVideo(video_public_id).video_thumbnail_url(width=300, height=300) + +def add_watermark_to_cloudinary_video(public_id, text="ARC"): + """إنشاء رابط فيديو مع علامة مائية نصية باستخدام التحويلات السحابية""" + transformed_url = cloudinary.CloudinaryVideo(public_id).video_url( + transformation=[ + {"width": 500, "crop": "scale"}, + {"overlay": {"font_family": "Arial", "font_size": 40, "text": text}, "gravity": "south_east", "opacity": 60} ] - for c in challenges_data: - db.execute(''' - INSERT INTO challenges (title, description, prize_coins, start_date, end_date) - VALUES (?, ?, ?, ?, ?) - ''', c) - db.commit() + ) + return transformed_url -# دوال الذكاء الاصطناعي +# ---------- دوال الذكاء الاصطناعي (تبقى كما هي) ---------- def analyze_video_nsfw(video_path): - """تحليل فيديو للكشف عن محتوى غير مناسب""" if not HAS_CV2: return 0.0 - try: cap = cv2.VideoCapture(video_path) if not cap.isOpened(): return 0.0 - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if total_frames == 0: cap.release() return 0.0 - sample_frames = min(10, total_frames) frame_indices = [int(i * total_frames / sample_frames) for i in range(sample_frames)] - skin_tone_count = 0 for idx in frame_indices: cap.set(cv2.CAP_PROP_POS_FRAMES, idx) @@ -632,153 +494,61 @@ def analyze_video_nsfw(video_path): skin_percentage = (cv2.countNonZero(mask) / (frame.shape[0] * frame.shape[1])) * 100 if skin_percentage > 30: skin_tone_count += 1 - cap.release() nsfw_score = skin_tone_count / sample_frames return min(nsfw_score, 1.0) - except Exception as e: logger.error(f"Error in analyze_video_nsfw: {e}") return 0.0 def extract_video_features(video_path): - """استخراج خصائص من الفيديو لتوليد متجه""" if not HAS_CV2: return random_vector() - try: cap = cv2.VideoCapture(video_path) - if not cap.isOpened(): - return random_vector() - ret, frame = cap.read() cap.release() - if not ret: return random_vector() - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) resized = cv2.resize(gray, (32, 32)) - features = resized.flatten().astype(np.float32) features = features / 255.0 - if len(features) > VECTOR_DIM: step = len(features) // VECTOR_DIM features = features[::step][:VECTOR_DIM] elif len(features) < VECTOR_DIM: features = np.pad(features, (0, VECTOR_DIM - len(features))) - return features.tobytes() - except Exception as e: logger.error(f"Error in extract_video_features: {e}") return random_vector() def generate_auto_tags(video_path, title, description): - """توليد وسوم تلقائية من النص وتحليل الفيديو""" tags = set() - text = f"{title} {description}" words = re.findall(r'#(\w+)', text) tags.update(words) - all_words = re.findall(r'[\w]+', text) common_tags = ['فيديو', 'ترند', 'مشاهير', 'comedy', 'funny', 'music', 'dance', 'tiktok'] - for word in all_words: word_lower = word.lower() if len(word_lower) > 2 and word_lower not in tags: if word_lower in common_tags or word_lower.isalpha(): tags.add(word_lower) - - if HAS_CV2 and os.path.exists(video_path): - try: - cap = cv2.VideoCapture(video_path) - if cap.isOpened(): - ret, frame = cap.read() - if ret: - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - brightness = np.mean(gray) - - if brightness > 200: - tags.add('مشرق') - tags.add('bright') - elif brightness < 50: - tags.add('داكن') - tags.add('dark') - - edges = cv2.Canny(gray, 50, 150) - edge_density = np.count_nonzero(edges) / edges.size - - if edge_density > 0.3: - tags.add('تفاصيل') - tags.add('details') - else: - tags.add('بسيط') - tags.add('simple') - - cap.release() - except: - pass - return json.dumps(list(tags)[:15]) -# دوال Cloudinary -def upload_to_cloudinary(file_path, folder='videos', resource_type='video'): - """رفع ملف إلى Cloudinary""" - if not HAS_CLOUDINARY: - return None, None - try: - response = cloudinary.uploader.upload( - file_path, - folder=folder, - resource_type=resource_type, - eager=[ - {'width': 720, 'height': 1280, 'crop': 'limit'}, - {'width': 360, 'height': 640, 'crop': 'limit'} - ], - eager_async=True - ) - return response['secure_url'], response['public_id'] - except Exception as e: - logger.error(f"Cloudinary upload error: {e}") - return None, None - -def get_cloudinary_video_url(public_id, watermark=False): - """الحصول على رابط فيديو من Cloudinary مع إمكانية إضافة علامة مائية""" - if watermark: - return cloudinary.CloudinaryVideo(public_id).build_url( - transformation=[ - {'width': 720, 'height': 1280, 'crop': 'limit'}, - {'overlay': {'font_family': 'Arial', 'font_size': 60, 'text': 'ARC'}, - 'color': '#ff2d55', 'gravity': 'south_east', 'x': 20, 'y': 20} - ] - ) - else: - return cloudinary.CloudinaryVideo(public_id).build_url() - -# دوال نقاط الخبرة والمكافآت -def calculate_level_from_xp(xp): - if xp < 100: - return 1 - return int((xp // 100) ** 0.5) + 1 - +# ---------- دوال نقاط الخبرة والمكافآت ---------- def add_xp(user_id, action, xp_amount): db = get_db() try: - db.execute(''' - INSERT INTO user_xp (user_id, action, xp_gained) - VALUES (?, ?, ?) - ''', (user_id, action, xp_amount)) - + db.execute('INSERT INTO user_xp (user_id, action, xp_gained) VALUES (?, ?, ?)', + (user_id, action, xp_amount)) db.execute('UPDATE users SET xp = xp + ? WHERE id = ?', (xp_amount, user_id)) - user = db.execute('SELECT xp FROM users WHERE id = ?', (user_id,)).fetchone() if user: - new_level = calculate_level_from_xp(user['xp']) + new_level = int((user['xp'] // 100) ** 0.5) + 1 db.execute('UPDATE users SET level = ? WHERE id = ?', (new_level, user_id)) - db.commit() return True except Exception as e: @@ -788,45 +558,26 @@ def add_xp(user_id, action, xp_amount): def claim_daily_reward(user_id): db = get_db() today = datetime.now().date() - try: - existing = db.execute(''' - SELECT id, streak FROM daily_rewards - WHERE user_id = ? AND reward_date = ? - ''', (user_id, today.isoformat())).fetchone() - + existing = db.execute('SELECT id FROM daily_rewards WHERE user_id = ? AND reward_date = ?', + (user_id, today.isoformat())).fetchone() if existing: return False, "تم استلام المكافأة اليوم مسبقاً" - yesterday = (today - timedelta(days=1)).isoformat() - last = db.execute(''' - SELECT streak FROM daily_rewards - WHERE user_id = ? AND reward_date = ? - ''', (user_id, yesterday)).fetchone() - + last = db.execute('SELECT streak FROM daily_rewards WHERE user_id = ? AND reward_date = ?', + (user_id, yesterday)).fetchone() streak = (last['streak'] + 1) if last else 1 coins = random.randint(50, 100) * streak - - db.execute(''' - INSERT INTO daily_rewards (user_id, reward_date, reward_coins, streak) - VALUES (?, ?, ?, ?) - ''', (user_id, today.isoformat(), coins, streak)) - + db.execute('INSERT INTO daily_rewards (user_id, reward_date, reward_coins, streak) VALUES (?, ?, ?, ?)', + (user_id, today.isoformat(), coins, streak)) db.execute('UPDATE users SET coins = coins + ? WHERE id = ?', (coins, user_id)) - - db.execute(''' - INSERT INTO transactions (user_id, type, amount, description) - VALUES (?, 'earn', ?, 'مكافأة يومية') - ''', (user_id, coins)) - db.commit() return True, coins, streak - except Exception as e: logger.error(f"Error claiming daily reward: {e}") return False, str(e) -# دوال التسويق بالعمولة +# ---------- دوال التسويق بالعمولة ---------- def generate_affiliate_link(user_id): code = secrets.token_urlsafe(8) db = get_db() @@ -840,21 +591,11 @@ def track_affiliate_click(referral_code, ip, user_agent): referrer = db.execute('SELECT id FROM users WHERE referral_code = ?', (referral_code,)).fetchone() if not referrer: return False - - db.execute(''' - INSERT INTO affiliate_clicks (referrer_id, ip, user_agent) - VALUES (?, ?, ?) - ''', (referrer['id'], ip, user_agent)) - + db.execute('INSERT INTO affiliate_clicks (referrer_id, ip, user_agent) VALUES (?, ?, ?)', + (referrer['id'], ip, user_agent)) db.execute('UPDATE users SET affiliate_clicks = affiliate_clicks + 1 WHERE id = ?', (referrer['id'],)) db.commit() - - log_file = os.path.join(AFFILIATE_LOG_FOLDER, f"clicks_{datetime.now().strftime('%Y%m')}.log") - with open(log_file, 'a') as f: - f.write(f"{datetime.now().isoformat()}|{referrer['id']}|{ip}|{user_agent}\n") - return True - except Exception as e: logger.error(f"Error tracking affiliate click: {e}") return False @@ -865,211 +606,26 @@ def process_affiliate_conversion(referral_code, new_user_id): referrer = db.execute('SELECT id FROM users WHERE referral_code = ?', (referral_code,)).fetchone() if not referrer: return - - db.execute(''' - UPDATE affiliate_clicks SET converted = 1 - WHERE referrer_id = ? AND converted = 0 - ORDER BY clicked_at DESC LIMIT 1 - ''', (referrer['id'],)) - + db.execute('UPDATE affiliate_clicks SET converted = 1 WHERE referrer_id = ? AND converted = 0 ORDER BY clicked_at DESC LIMIT 1', + (referrer['id'],)) db.execute('UPDATE users SET affiliate_conversions = affiliate_conversions + 1 WHERE id = ?', (referrer['id'],)) - coins_referrer = 100 coins_referred = 50 - db.execute('UPDATE users SET coins = coins + ?, affiliate_balance = affiliate_balance + ? WHERE id = ?', - (coins_referrer, coins_referrer, referrer['id'])) + (coins_referrer, coins_referrer, referrer['id'])) db.execute('UPDATE users SET coins = coins + ? WHERE id = ?', (coins_referred, new_user_id)) - - db.execute(''' - INSERT INTO transactions (user_id, type, amount, description) - VALUES (?, 'affiliate', ?, 'مكافأة إحالة') - ''', (referrer['id'], coins_referrer)) - - db.execute(''' - INSERT INTO transactions (user_id, type, amount, description) - VALUES (?, 'affiliate', ?, 'مكافأة تسجيل عن طريق رابط') - ''', (new_user_id, coins_referred)) - - db.execute(''' - INSERT INTO referrals (referrer_id, referred_id, reward_coins) - VALUES (?, ?, ?) - ''', (referrer['id'], new_user_id, coins_referrer)) - + db.execute('INSERT INTO referrals (referrer_id, referred_id, reward_coins) VALUES (?, ?, ?)', + (referrer['id'], new_user_id, coins_referrer)) db.commit() - - log_file = os.path.join(AFFILIATE_LOG_FOLDER, f"conversions_{datetime.now().strftime('%Y%m')}.log") - with open(log_file, 'a') as f: - f.write(f"{datetime.now().isoformat()}|{referrer['id']}|{new_user_id}\n") - except Exception as e: logger.error(f"Error processing affiliate conversion: {e}") -# دوال التحليلات +# ---------- دوال التحليلات ---------- def track_event(user_id, event_name, event_data=None): - db = get_db() - try: - db.execute(''' - INSERT INTO analytics_events (user_id, event_name, event_data) - VALUES (?, ?, ?) - ''', (user_id, event_name, json.dumps(event_data) if event_data else None)) - db.commit() - except Exception as e: - logger.error(f"Error tracking event: {e}") - -def get_user_stats(user_id): - db = get_db() - try: - videos = db.execute('SELECT COUNT(*) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] - total_views = db.execute('SELECT SUM(views) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] or 0 - total_likes = db.execute('SELECT SUM(likes_count) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] or 0 - total_comments = db.execute('SELECT SUM(comments_count) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] or 0 - followers = db.execute('SELECT COUNT(*) FROM follows WHERE user_id = ?', (user_id,)).fetchone()[0] - following = db.execute('SELECT COUNT(*) FROM follows WHERE follower_id = ?', (user_id,)).fetchone()[0] - - user = db.execute(''' - SELECT xp, level, coins, diamonds, affiliate_balance, affiliate_clicks, affiliate_conversions - FROM users WHERE id = ? - ''', (user_id,)).fetchone() - - return { - 'videos': videos, - 'total_views': total_views, - 'total_likes': total_likes, - 'total_comments': total_comments, - 'followers': followers, - 'following': following, - 'xp': user['xp'], - 'level': user['level'], - 'coins': user['coins'], - 'diamonds': user['diamonds'], - 'affiliate_balance': user['affiliate_balance'], - 'affiliate_clicks': user['affiliate_clicks'], - 'affiliate_conversions': user['affiliate_conversions'] - } - except Exception as e: - logger.error(f"Error getting user stats: {e}") - return {} - -# دوال المطور -def create_developer_session(user_id): - token = secrets.token_urlsafe(32) - expires = datetime.now() + timedelta(hours=1) - db = get_db() - db.execute(''' - INSERT INTO developer_sessions (user_id, session_token, expires_at) - VALUES (?, ?, ?) - ''', (user_id, token, expires.isoformat())) - db.commit() - return token - -def validate_developer_session(token): - db = get_db() - session = db.execute(''' - SELECT user_id, expires_at FROM developer_sessions - WHERE session_token = ? AND expires_at > ? - ''', (token, datetime.now().isoformat())).fetchone() - - if session: - return session['user_id'] - return None - -def execute_code_in_sandbox(code): - local_vars = {} - try: - allowed_builtins = { - 'print': print, - 'len': len, - 'range': range, - 'int': int, - 'str': str, - 'float': float, - 'list': list, - 'dict': dict, - 'set': set, - 'tuple': tuple, - 'bool': bool, - 'enumerate': enumerate, - 'zip': zip, - 'sum': sum, - 'max': max, - 'min': min, - 'abs': abs, - 'round': round, - 'sorted': sorted, - 'reversed': reversed, - 'any': any, - 'all': all, - } - - exec(code, {"__builtins__": allowed_builtins}, local_vars) - - result = None - if 'result' in local_vars: - result = local_vars['result'] - elif len(local_vars) > 0: - last_var = list(local_vars.keys())[-1] - result = local_vars[last_var] - - return {"success": True, "result": str(result) if result is not None else None} - - except Exception as e: - return {"success": False, "error": str(e)} - -def install_plugin(user_id, name, code): - db = get_db() - try: - db.execute('DELETE FROM developer_plugins WHERE user_id = ? AND name = ?', (user_id, name)) - - db.execute(''' - INSERT INTO developer_plugins (user_id, name, code, filename) - VALUES (?, ?, ?, ?) - ''', (user_id, name, code, name + '.py')) - - db.commit() - - load_plugin_into_memory(user_id, name, code) - - return True - - except Exception as e: - logger.error(f"Error installing plugin: {e}") - return False + # يمكن تنفيذها لاحقاً + pass -def load_plugin_into_memory(user_id, name, code): - try: - namespace = {} - exec(code, namespace) - developer_plugins[f"{user_id}_{name}"] = namespace - logger.info(f"Plugin {name} loaded for user {user_id}") - return True - except Exception as e: - logger.error(f"Error loading plugin {name}: {e}") - return False - -def load_all_plugins(): - db = get_db() - try: - plugins = db.execute('SELECT user_id, name, code FROM developer_plugins WHERE is_active = 1').fetchall() - for p in plugins: - load_plugin_into_memory(p['user_id'], p['name'], p['code']) - logger.info(f"Loaded {len(plugins)} plugins") - except Exception as e: - logger.error(f"Error loading plugins: {e}") - -def call_plugin_hook(user_id, hook_name, *args, **kwargs): - results = [] - for key, namespace in developer_plugins.items(): - uid = int(key.split('_')[0]) - if uid == user_id and hook_name in namespace: - try: - res = namespace[hook_name](*args, **kwargs) - results.append(res) - except Exception as e: - logger.error(f"Plugin hook {hook_name} error: {e}") - return results - -# دوال الإشعارات والعملات +# ---------- دوال الإشعارات والعملات ---------- def add_notification(user_id, from_user_id, n_type, content, video_id=None, comment_id=None): if user_id == from_user_id: return @@ -1079,7 +635,6 @@ def add_notification(user_id, from_user_id, n_type, content, video_id=None, comm VALUES (?, ?, ?, ?, ?, ?) ''', (user_id, from_user_id, n_type, content, video_id, comment_id)) db.commit() - if user_id in notification_queues: notification_queues[user_id].put({ 'type': n_type, @@ -1092,10 +647,8 @@ def add_notification(user_id, from_user_id, n_type, content, video_id=None, comm def add_coins(user_id, amount, description): db = get_db() db.execute('UPDATE users SET coins = coins + ? WHERE id = ?', (amount, user_id)) - db.execute(''' - INSERT INTO transactions (user_id, type, amount, description) - VALUES (?, 'earn', ?, ?) - ''', (user_id, amount, description)) + db.execute('INSERT INTO transactions (user_id, type, amount, description) VALUES (?, "earn", ?, ?)', + (user_id, amount, description)) db.commit() def deduct_coins(user_id, amount, description): @@ -1103,15 +656,13 @@ def deduct_coins(user_id, amount, description): user = db.execute('SELECT coins FROM users WHERE id = ?', (user_id,)).fetchone() if user and user['coins'] >= amount: db.execute('UPDATE users SET coins = coins - ? WHERE id = ?', (amount, user_id)) - db.execute(''' - INSERT INTO transactions (user_id, type, amount, description) - VALUES (?, 'spend', ?, ?) - ''', (user_id, amount, description)) + db.execute('INSERT INTO transactions (user_id, type, amount, description) VALUES (?, "spend", ?, ?)', + (user_id, amount, description)) db.commit() return True return False -# نظام التوصيات +# ---------- نظام التوصيات ---------- def get_user_interest_vector(user_id): db = get_db() rows = db.execute(''' @@ -1121,29 +672,20 @@ def get_user_interest_vector(user_id): ORDER BY i.timestamp DESC LIMIT 50 ''', (user_id,)).fetchall() - if not rows: return np.zeros(VECTOR_DIM, dtype=np.float32) - vectors = [bytes_to_vector(row['vector']) for row in rows if row['vector']] if not vectors: return np.zeros(VECTOR_DIM, dtype=np.float32) - return np.mean(vectors, axis=0) -def recommend_videos(user_id, feed_type='for-you', limit=10, offset=0): +def recommend_videos(user_id, limit=10, offset=0): db = get_db() try: watched = db.execute('SELECT video_id FROM interactions WHERE user_id = ? AND watched = 1', (user_id,)).fetchall() watched_ids = [w['video_id'] for w in watched] if watched else [-1] - - following_ids = [] - if feed_type == 'friends': - following = db.execute('SELECT user_id FROM follows WHERE follower_id = ?', (user_id,)).fetchall() - following_ids = [f['user_id'] for f in following] if following else [-1] - - interest = get_user_interest_vector(user_id) - + following = db.execute('SELECT user_id FROM follows WHERE follower_id = ?', (user_id,)).fetchall() + following_ids = [f['user_id'] for f in following] if following else [-1] placeholders = ','.join(['?'] * len(watched_ids)) query = f''' SELECT v.*, u.username, u.avatar, u.is_verified, @@ -1153,48 +695,19 @@ def recommend_videos(user_id, feed_type='for-you', limit=10, offset=0): JOIN users u ON v.user_id = u.id WHERE v.id NOT IN ({placeholders}) AND v.visibility = 'public' + ORDER BY v.upload_time DESC + LIMIT ? OFFSET ? ''' - params = watched_ids - - if feed_type == 'friends': - query += f' AND v.user_id IN ({",".join(["?"] * len(following_ids))})' - params += following_ids - - query += ' ORDER BY v.upload_time DESC LIMIT ? OFFSET ?' - params += [limit * 3, offset] - + params = watched_ids + [limit * 3, offset] rows = db.execute(query, params).fetchall() - - if feed_type == 'for-you': - video_scores = [] - for row in rows: - score = 10.0 - - if not np.all(interest == 0): - vec_row = db.execute('SELECT vector FROM videos WHERE id = ?', (row['id'],)).fetchone() - if vec_row and vec_row['vector']: - vec = bytes_to_vector(vec_row['vector']) - similarity = 1.0 / (1.0 + np.linalg.norm(interest - vec)) - score += similarity * 30 - - popularity = (row['views'] * 2 + row['likes_count'] * 5 + row['comments_count'] * 3) / 100 - score += min(popularity, 40) - - upload_time = datetime.strptime(row['upload_time'], '%Y-%m-%d %H:%M:%S') - hours_ago = (datetime.now() - upload_time).total_seconds() / 3600 - recency = max(0, 20 - (hours_ago / 72)) - score += recency - - if row['user_id'] in following_ids: - score += 10 - - video_scores.append((score, dict(row))) - - video_scores.sort(key=lambda x: x[0], reverse=True) - return [v for _, v in video_scores[:limit]] - else: - return [dict(row) for row in rows[:limit]] - + videos = [] + for row in rows: + v = dict(row) + if v['user_id'] in following_ids: + videos.insert(0, v) + else: + videos.append(v) + return videos[:limit] except Exception as e: logger.error(f"Error in recommendation: {e}") rows = db.execute(''' @@ -1207,7 +720,25 @@ def recommend_videos(user_id, feed_type='for-you', limit=10, offset=0): ''', (limit, offset)).fetchall() return [dict(row) for row in rows] -# ديكوريتورات +def recommend_friends_videos(user_id, limit=10, offset=0): + """توصية فيديوهات الأصدقاء (المتابَعين)""" + db = get_db() + try: + videos = db.execute(''' + SELECT v.*, u.username, u.avatar, u.is_verified + FROM videos v + JOIN users u ON v.user_id = u.id + WHERE v.user_id IN (SELECT user_id FROM follows WHERE follower_id = ?) + AND v.visibility = 'public' + ORDER BY v.upload_time DESC + LIMIT ? OFFSET ? + ''', (user_id, limit, offset)).fetchall() + return [dict(v) for v in videos] + except Exception as e: + logger.error(f"Error in friends videos: {e}") + return [] + +# ---------- ديكوريتورات ---------- def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): @@ -1230,24 +761,12 @@ def admin_required(f): return f(*args, **kwargs) return decorated_function -def developer_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if 'user_id' not in session: - return redirect(url_for('login')) - db = get_db() - user = db.execute('SELECT is_developer FROM users WHERE id = ?', (session['user_id'],)).fetchone() - if not user or not user['is_developer']: - abort(403) - return f(*args, **kwargs) - return decorated_function - -# صفحات تسجيل الدخول والتسجيل +# ---------- صفحات تسجيل الدخول والتسجيل ---------- @app.route('/') def index(): if 'user_id' not in session: return redirect(url_for('login')) - return render_template_string(SPA_TEMPLATE) + return render_template_string(MAIN_TEMPLATE) @app.route('/login', methods=['GET', 'POST']) def login(): @@ -1255,320 +774,165 @@ def login(): username = request.form.get('username', '') password = request.form.get('password', '') remember = request.form.get('remember', False) - db = get_db() user = db.execute('SELECT * FROM users WHERE username = ? OR email = ?', (username, username)).fetchone() - if user and check_password_hash(user['password_hash'], password): session['user_id'] = user['id'] session['username'] = user['username'] session['is_admin'] = user['is_admin'] - session['is_developer'] = user['is_developer'] - if remember: session.permanent = True - db.execute('UPDATE users SET last_active = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],)) db.commit() - - track_event(user['id'], 'login', {}) - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({'success': True, 'redirect': '/'}) return redirect(url_for('index')) - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({'success': False, 'error': 'بيانات الدخول غير صحيحة'}), 401 return render_template_string(LOGIN_PAGE, error='اسم المستخدم أو كلمة المرور غير صحيحة') - return render_template_string(LOGIN_PAGE) @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': - if request.is_json: - data = request.get_json() - username = data.get('username', '').strip() - email = data.get('email', '').strip() - password = data.get('password', '') - confirm = data.get('confirm_password', '') - referral = data.get('referral_code', '').strip() - else: - username = request.form.get('username', '').strip() - email = request.form.get('email', '').strip() - password = request.form.get('password', '') - confirm = request.form.get('confirm_password', '') - referral = request.form.get('referral_code', '').strip() - + username = request.form.get('username', '').strip() + email = request.form.get('email', '').strip() + password = request.form.get('password', '') + confirm = request.form.get('confirm_password', '') + referral = request.form.get('referral_code', '').strip() if not username or not email or not password: - return jsonify({'error': 'جميع الحقول مطلوبة'}), 400 - + return render_template_string(REGISTER_PAGE, error='جميع الحقول مطلوبة') if password != confirm: - return jsonify({'error': 'كلمات المرور غير متطابقة'}), 400 - + return render_template_string(REGISTER_PAGE, error='كلمات المرور غير متطابقة') if len(password) < 6: - return jsonify({'error': 'كلمة المرور يجب أن تكون 6 أحرف على الأقل'}), 400 - + return render_template_string(REGISTER_PAGE, error='كلمة المرور يجب أن تكون 6 أحرف على الأقل') db = get_db() existing = db.execute('SELECT id FROM users WHERE username = ? OR email = ?', (username, email)).fetchone() if existing: - return jsonify({'error': 'اسم المستخدم أو البريد الإلكتروني موجود بالفعل'}), 400 - + return render_template_string(REGISTER_PAGE, error='اسم المستخدم أو البريد الإلكتروني موجود بالفعل') hashed = generate_password_hash(password) referral_code = generate_referral_code() - db.execute(''' INSERT INTO users (username, email, password_hash, referral_code) VALUES (?, ?, ?, ?) ''', (username, email, hashed, referral_code)) - user_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] db.execute('INSERT INTO user_settings (user_id) VALUES (?)', (user_id,)) - if referral: referrer = db.execute('SELECT id FROM users WHERE referral_code = ?', (referral,)).fetchone() if referrer: process_affiliate_conversion(referral, user_id) - add_xp(user_id, 'register', 50) - track_event(user_id, 'register', {'referral': referral if referral else None}) - db.commit() - - if request.is_json: - return jsonify({'success': True, 'redirect': '/login'}) return redirect(url_for('login')) - return render_template_string(REGISTER_PAGE) @app.route('/logout') def logout(): - track_event(session.get('user_id'), 'logout', {}) session.clear() return redirect(url_for('login')) -@app.route('/forgot-password', methods=['GET', 'POST']) -def forgot_password(): - if request.method == 'POST': - email = request.form.get('email', '') - return render_template_string(FORGOT_PAGE, message='تم إرسال رابط إعادة التعيين إلى بريدك الإلك��روني') - return render_template_string(FORGOT_PAGE) - -# مسارات API للملف الشخصي -@app.route('/api/profile/') -@login_required -def api_profile(user_id): - db = get_db() - user = db.execute(''' - SELECT u.*, s.privacy_profile, s.privacy_videos - FROM users u - LEFT JOIN user_settings s ON u.id = s.user_id - WHERE u.id = ? - ''', (user_id,)).fetchone() - - if not user: - return jsonify({'error': 'User not found'}), 404 - - videos = db.execute(''' - SELECT id, title, cloudinary_url, thumbnail, views, likes_count, comments_count, upload_time - FROM videos WHERE user_id = ? AND visibility = 'public' - ORDER BY upload_time DESC LIMIT 30 - ''', (user_id,)).fetchall() - - followers_count = db.execute('SELECT COUNT(*) FROM follows WHERE user_id = ?', (user_id,)).fetchone()[0] - following_count = db.execute('SELECT COUNT(*) FROM follows WHERE follower_id = ?', (user_id,)).fetchone()[0] - - is_following = False - if session['user_id'] != user_id: - is_following = db.execute('SELECT * FROM follows WHERE user_id = ? AND follower_id = ?', - (user_id, session['user_id'])).fetchone() is not None - - total_views = db.execute('SELECT SUM(views) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] or 0 - total_likes = db.execute('SELECT SUM(likes_count) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] or 0 - - videos_list = [] - for v in videos: - video_dict = dict(v) - videos_list.append(video_dict) - - return jsonify({ - 'id': user['id'], - 'username': user['username'], - 'avatar': f"/avatars/{user['avatar']}", - 'cover': f"/avatars/{user['cover_image']}", - 'bio': user['bio'], - 'is_verified': user['is_verified'], - 'is_following': is_following, - 'followers': followers_count, - 'following': following_count, - 'videos_count': len(videos_list), - 'total_views': total_views, - 'total_likes': total_likes, - 'videos': videos_list - }) - -@app.route('/api/profile//saved') -@login_required -def api_saved_videos(user_id): - if user_id != session['user_id']: - return jsonify({'error': 'Forbidden'}), 403 - - db = get_db() - videos = db.execute(''' - SELECT v.*, u.username, u.avatar - FROM saved_videos sv - JOIN videos v ON sv.video_id = v.id - JOIN users u ON v.user_id = u.id - WHERE sv.user_id = ? - ORDER BY sv.saved_at DESC - ''', (user_id,)).fetchall() - - result = [] - for v in videos: - video = dict(v) - video['avatar_url'] = f"/avatars/{v['avatar']}" - result.append(video) - - return jsonify(result) - -@app.route('/api/follow/', methods=['POST']) +# ---------- نظام المتابعة (الأصدقاء) ---------- +@app.route('/follow/', methods=['POST']) @login_required -def api_follow(user_id): +def follow(user_id): if user_id == session['user_id']: return jsonify({'error': 'لا يمكنك متابعة نفسك'}), 400 - db = get_db() db.execute('INSERT OR IGNORE INTO follows (user_id, follower_id) VALUES (?, ?)', (user_id, session['user_id'])) db.commit() - user = db.execute('SELECT username FROM users WHERE id = ?', (session['user_id'],)).fetchone() add_notification(user_id, session['user_id'], 'follow', f'بدأ {user["username"]} بمتابعتك') - count = db.execute('SELECT COUNT(*) FROM follows WHERE user_id = ?', (user_id,)).fetchone()[0] - add_xp(session['user_id'], 'follow', 5) - track_event(session['user_id'], 'follow', {'followed_user_id': user_id}) - return jsonify({'status': 'ok', 'followers_count': count}) -@app.route('/api/unfollow/', methods=['POST']) +@app.route('/unfollow/', methods=['POST']) @login_required -def api_unfollow(user_id): +def unfollow(user_id): db = get_db() db.execute('DELETE FROM follows WHERE user_id = ? AND follower_id = ?', (user_id, session['user_id'])) db.commit() - count = db.execute('SELECT COUNT(*) FROM follows WHERE user_id = ?', (user_id,)).fetchone()[0] - track_event(session['user_id'], 'unfollow', {'unfollowed_user_id': user_id}) - return jsonify({'status': 'ok', 'followers_count': count}) -# مسارات الفيديو -@app.route('/api/feed/') +@app.route('/api/followers/') @login_required -def api_feed(feed_type): - try: - page = int(request.args.get('page', 1)) - limit = 10 - offset = (page - 1) * limit - - videos = recommend_videos(session['user_id'], feed_type, limit, offset) - - result = [] - for v in videos: - video = {} - for key, value in v.items(): - if key != 'vector' and not isinstance(value, bytes): - video[key] = value - elif key == 'vector': - continue - else: - try: - video[key] = str(value) - except: - video[key] = None - - video['avatar_url'] = f"/avatars/{v['avatar']}" - - db = get_db() - interaction = db.execute(''' - SELECT liked FROM interactions WHERE user_id = ? AND video_id = ? - ''', (session['user_id'], v['id'])).fetchone() - - video['liked_by_user'] = bool(interaction['liked']) if interaction else False - - saved = db.execute('SELECT * FROM saved_videos WHERE user_id = ? AND video_id = ?', - (session['user_id'], v['id'])).fetchone() - video['saved_by_user'] = saved is not None - - result.append(video) - - track_event(session['user_id'], f'view_feed_{feed_type}', {'page': page}) - - return jsonify({'videos': result, 'next_page': page + 1 if len(result) == limit else None}) - - except Exception as e: - logger.error(f"Error in feed: {e}") - return jsonify({'videos': [], 'next_page': None, 'error': str(e)}), 500 +def get_followers(user_id): + db = get_db() + followers = db.execute(''' + SELECT u.id, u.username, u.avatar, u.is_verified + FROM follows f + JOIN users u ON f.follower_id = u.id + WHERE f.user_id = ? + ORDER BY f.created_at DESC + ''', (user_id,)).fetchall() + result = [] + for f in followers: + follower = dict(f) + follower['avatar_url'] = f'/avatars/{f["avatar"]}' + result.append(follower) + return jsonify(result) -@app.route('/api/upload', methods=['POST']) +@app.route('/api/following/') @login_required -def api_upload(): +def get_following(user_id): + db = get_db() + following = db.execute(''' + SELECT u.id, u.username, u.avatar, u.is_verified + FROM follows f + JOIN users u ON f.user_id = u.id + WHERE f.follower_id = ? + ORDER BY f.created_at DESC + ''', (user_id,)).fetchall() + result = [] + for f in following: + follow = dict(f) + follow['avatar_url'] = f'/avatars/{f["avatar"]}' + result.append(follow) + return jsonify(result) + +# ---------- نظام الفيديوهات مع Cloudinary ---------- +@app.route('/upload', methods=['GET', 'POST']) +@login_required +def upload_video(): + if request.method == 'GET': + return render_template_string(UPLOAD_TEMPLATE) if 'video' not in request.files: - return jsonify({'error': 'لم يتم اختيار فيديو'}), 400 - + return 'لم يتم اختيار فيديو', 400 file = request.files['video'] if file.filename == '': - return jsonify({'error': 'لم يتم اختيار فيديو'}), 400 - + return 'لم يتم اختيار فيديو', 400 title = request.form.get('title', '') description = request.form.get('description', '') music = request.form.get('music', '') - challenge_id = request.form.get('challenge_id') allow_comments = request.form.get('allow_comments') == 'on' allow_duet = request.form.get('allow_duet') == 'on' allow_stitch = request.form.get('allow_stitch') == 'on' visibility = request.form.get('visibility', 'public') - if file and allowed_video(file.filename): filename = secure_filename(file.filename) - unique_name = f"{uuid.uuid4().hex}_{filename}" - temp_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_name) + temp_path = os.path.join('/tmp', f"{uuid.uuid4().hex}_{filename}") file.save(temp_path) - - # استخراج الخصائص - vec_bytes = extract_video_features(temp_path) - - # تحليل NSFW + cloudinary_url, cloudinary_public_id = upload_video_to_cloudinary(temp_path) + if not cloudinary_url: + os.remove(temp_path) + return 'فشل رفع الفيديو إلى السحابة', 500 + thumbnail_url = generate_video_thumbnail(cloudinary_public_id) + vec_bytes = extract_video_features(temp_path) if HAS_CV2 else random_vector() nsfw_score = analyze_video_nsfw(temp_path) if HAS_CV2 else 0.0 - - # توليد وسوم تلقائية ai_tags = generate_auto_tags(temp_path, title, description) - - # رفع إلى Cloudinary - cloud_url, public_id = upload_to_cloudinary(temp_path) - - if not cloud_url: - os.remove(temp_path) - return jsonify({'error': 'فشل رفع الفيديو إلى السحابة'}), 500 - - # حذف الملف المؤقت - os.remove(temp_path) - db = get_db() cursor = db.execute(''' INSERT INTO videos ( - user_id, cloudinary_url, public_id, title, description, music, - allow_comments, allow_duet, allow_stitch, visibility, vector, challenge_id, + user_id, cloudinary_url, cloudinary_public_id, thumbnail_url, title, description, music, + allow_comments, allow_duet, allow_stitch, visibility, vector, nsfw_score, ai_tags ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (session['user_id'], cloud_url, public_id, title, description, music, - allow_comments, allow_duet, allow_stitch, visibility, vec_bytes, challenge_id, + ''', (session['user_id'], cloudinary_url, cloudinary_public_id, thumbnail_url, title, description, music, + allow_comments, allow_duet, allow_stitch, visibility, vec_bytes, nsfw_score, ai_tags)) - video_id = cursor.lastrowid - - # معالجة الهاشتاغات hashtags = re.findall(r'#(\w+)', description) for tag in hashtags: tag_lower = tag.lower() @@ -1578,36 +942,166 @@ def api_upload(): db.execute('INSERT OR IGNORE INTO video_hashtags (video_id, hashtag_id) VALUES (?, ?)', (video_id, tag_row['id'])) db.execute('UPDATE hashtags SET usage_count = usage_count + 1 WHERE id = ?', (tag_row['id'],)) - - # معالجة التحدي - if challenge_id and challenge_id.isdigit(): - db.execute(''' - INSERT INTO challenge_participants (challenge_id, user_id, video_id) - VALUES (?, ?, ?) - ''', (int(challenge_id), session['user_id'], video_id)) - db.commit() - + os.remove(temp_path) add_coins(session['user_id'], 10, 'مكافأة نشر فيديو') add_xp(session['user_id'], 'upload_video', 20) - track_event(session['user_id'], 'upload_video', {'video_id': video_id}) - - call_plugin_hook(session['user_id'], 'on_video_upload', video_id) - - return jsonify({'success': True, 'video_id': video_id}) - - return jsonify({'error': 'نوع الملف غير مدعوم'}), 400 + return redirect(url_for('index')) + return 'نوع الملف غير مدعوم', 400 + +@app.route('/api/for-you') +@login_required +def for_you(): + try: + page = int(request.args.get('page', 1)) + limit = 10 + offset = (page - 1) * limit + videos = recommend_videos(session['user_id'], limit, offset) + result = [] + for v in videos: + video = {} + for key, value in v.items(): + if key != 'vector' and not isinstance(value, bytes): + video[key] = value + elif key == 'vector': + continue + else: + try: + video[key] = str(value) + except: + video[key] = None + video['url'] = v['cloudinary_url'] + video['avatar_url'] = f'/avatars/{v["avatar"]}' + db = get_db() + interaction = db.execute('SELECT liked FROM interactions WHERE user_id = ? AND video_id = ?', + (session['user_id'], v['id'])).fetchone() + video['liked_by_user'] = bool(interaction['liked']) if interaction else False + result.append(video) + return jsonify({'videos': result, 'next_page': page + 1 if len(result) == limit else None}) + except Exception as e: + logger.error(f"Error in for_you: {e}") + return jsonify({'videos': [], 'next_page': None, 'error': str(e)}), 500 + +@app.route('/api/friends') +@login_required +def friends_feed(): + try: + page = int(request.args.get('page', 1)) + limit = 10 + offset = (page - 1) * limit + videos = recommend_friends_videos(session['user_id'], limit, offset) + result = [] + for v in videos: + video = dict(v) + video['url'] = v['cloudinary_url'] + video['avatar_url'] = f'/avatars/{v["avatar"]}' + video.pop('vector', None) + db = get_db() + interaction = db.execute('SELECT liked FROM interactions WHERE user_id = ? AND video_id = ?', + (session['user_id'], v['id'])).fetchone() + video['liked_by_user'] = bool(interaction['liked']) if interaction else False + result.append(video) + return jsonify({'videos': result, 'next_page': page + 1 if len(result) == limit else None}) + except Exception as e: + logger.error(f"Error in friends_feed: {e}") + return jsonify({'videos': [], 'next_page': None}), 500 + +@app.route('/api/trending') +@login_required +def trending(): + try: + page = int(request.args.get('page', 1)) + limit = 10 + offset = (page - 1) * limit + db = get_db() + videos = db.execute(''' + SELECT v.*, u.username, u.avatar, u.is_verified, + (SELECT COUNT(*) FROM interactions WHERE video_id = v.id AND liked = 1) as likes + FROM videos v + JOIN users u ON v.user_id = u.id + WHERE v.visibility = 'public' + ORDER BY v.views DESC, v.likes_count DESC + LIMIT ? OFFSET ? + ''', (limit, offset)).fetchall() + result = [] + for v in videos: + video = dict(v) + video['url'] = v['cloudinary_url'] + video['avatar_url'] = f'/avatars/{v["avatar"]}' + video.pop('vector', None) + interaction = db.execute('SELECT liked FROM interactions WHERE user_id = ? AND video_id = ?', + (session['user_id'], v['id'])).fetchone() + video['liked_by_user'] = bool(interaction['liked']) if interaction else False + result.append(video) + return jsonify({'videos': result, 'next_page': page + 1 if len(result) == limit else None}) + except Exception as e: + logger.error(f"Error in trending: {e}") + return jsonify({'videos': [], 'next_page': None}), 500 + +@app.route('/api/search') +@login_required +def search(): + try: + q = request.args.get('q', '').strip() + db = get_db() + if not q: + videos = db.execute(''' + SELECT v.*, u.username, u.avatar + FROM videos v + JOIN users u ON v.user_id = u.id + WHERE v.visibility = 'public' + ORDER BY RANDOM() + LIMIT 30 + ''').fetchall() + else: + videos = db.execute(''' + SELECT v.*, u.username, u.avatar + FROM videos v + JOIN users u ON v.user_id = u.id + WHERE v.visibility = 'public' + AND (v.title LIKE ? OR v.description LIKE ?) + ORDER BY v.views DESC + LIMIT 30 + ''', ('%'+q+'%', '%'+q+'%')).fetchall() + hashtag = db.execute('SELECT id FROM hashtags WHERE tag LIKE ?', ('%'+q+'%',)).fetchone() + if hashtag: + hashtag_videos = db.execute(''' + SELECT v.*, u.username, u.avatar + FROM videos v + JOIN users u ON v.user_id = u.id + JOIN video_hashtags vh ON v.id = vh.video_id + WHERE vh.hashtag_id = ? AND v.visibility = 'public' + ORDER BY v.views DESC + LIMIT 20 + ''', (hashtag['id'],)).fetchall() + videos = list(videos) + list(hashtag_videos) + seen = set() + unique_videos = [] + for v in videos: + if v['id'] not in seen: + seen.add(v['id']) + unique_videos.append(v) + videos = unique_videos[:30] + result = [] + for v in videos: + video = dict(v) + video['url'] = v['cloudinary_url'] + video['avatar_url'] = f'/avatars/{v["avatar"]}' + video.pop('vector', None) + result.append(video) + return jsonify({'videos': result}) + except Exception as e: + logger.error(f"Error in search: {e}") + return jsonify({'videos': []}), 500 @app.route('/api/like/', methods=['POST']) @login_required -def api_like(video_id): +def like_video(video_id): try: user_id = session['user_id'] db = get_db() - interaction = db.execute('SELECT liked FROM interactions WHERE user_id = ? AND video_id = ?', (user_id, video_id)).fetchone() - if interaction and interaction['liked']: db.execute('UPDATE interactions SET liked = 0 WHERE user_id = ? AND video_id = ?', (user_id, video_id)) db.execute('UPDATE videos SET likes_count = likes_count - 1 WHERE id = ?', (video_id,)) @@ -1620,42 +1114,31 @@ def api_like(video_id): ''', (user_id, video_id)) db.execute('UPDATE videos SET likes_count = likes_count + 1 WHERE id = ?', (video_id,)) liked = True - video = db.execute('SELECT user_id FROM videos WHERE id = ?', (video_id,)).fetchone() if video and video['user_id'] != user_id: user = db.execute('SELECT username FROM users WHERE id = ?', (user_id,)).fetchone() if user: add_notification(video['user_id'], user_id, 'like', f'أعجب {user["username"]} بفيديو لك', video_id) - db.commit() - likes_count = db.execute('SELECT likes_count FROM videos WHERE id = ?', (video_id,)).fetchone()['likes_count'] - if liked: add_xp(user_id, 'like', 2) - - track_event(user_id, 'like', {'video_id': video_id, 'liked': liked}) - return jsonify({'status': 'ok', 'liked': liked, 'likes_count': likes_count}) - except Exception as e: logger.error(f"Error in like_video: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/view/', methods=['POST']) @login_required -def api_view(video_id): +def view_video(video_id): try: user_id = session['user_id'] data = request.get_json() or {} watch_time = data.get('watch_time', 1) completed = data.get('completed', False) - db = get_db() - existing = db.execute('SELECT watched FROM interactions WHERE user_id = ? AND video_id = ?', (user_id, video_id)).fetchone() - if not existing or not existing['watched']: db.execute(''' INSERT INTO interactions (user_id, video_id, watched, watch_time) @@ -1666,166 +1149,179 @@ def api_view(video_id): ''', (user_id, video_id, watch_time, watch_time)) db.execute('UPDATE videos SET views = views + 1 WHERE id = ?', (video_id,)) else: - db.execute(''' - UPDATE interactions SET watch_time = watch_time + ? WHERE user_id = ? AND video_id = ? - ''', (watch_time, user_id, video_id)) - - db.execute(''' - INSERT INTO video_views (user_id, video_id, watch_time, completed) - VALUES (?, ?, ?, ?) - ''', (user_id, video_id, watch_time, completed)) - + db.execute('UPDATE interactions SET watch_time = watch_time + ? WHERE user_id = ? AND video_id = ?', + (watch_time, user_id, video_id)) db.commit() - if completed: add_xp(user_id, 'complete_view', 3) - - track_event(user_id, 'view_video', {'video_id': video_id, 'watch_time': watch_time, 'completed': completed}) - return jsonify({'status': 'ok'}) - except Exception as e: logger.error(f"Error in view_video: {e}") return jsonify({'error': str(e)}), 500 +@app.route('/api/share/', methods=['POST']) +@login_required +def share_video(video_id): + try: + user_id = session['user_id'] + db = get_db() + db.execute(''' + INSERT INTO interactions (user_id, video_id, shared, watched) + VALUES (?, ?, 1, 1) + ON CONFLICT(user_id, video_id) DO UPDATE SET shared = 1 + ''', (user_id, video_id)) + db.execute('UPDATE videos SET shares_count = shares_count + 1 WHERE id = ?', (video_id,)) + db.commit() + add_coins(user_id, 5, 'مكافأة مشاركة فيديو') + add_xp(user_id, 'share', 3) + return jsonify({'status': 'ok'}) + except Exception as e: + logger.error(f"Error in share_video: {e}") + return jsonify({'error': str(e)}), 500 + @app.route('/api/save/', methods=['POST']) @login_required -def api_save(video_id): +def save_video(video_id): try: user_id = session['user_id'] db = get_db() - - saved = db.execute('SELECT * FROM saved_videos WHERE user_id = ? AND video_id = ?', + saved = db.execute('SELECT saved FROM interactions WHERE user_id = ? AND video_id = ?', (user_id, video_id)).fetchone() - - if saved: - db.execute('DELETE FROM saved_videos WHERE user_id = ? AND video_id = ?', (user_id, video_id)) + if saved and saved['saved']: + db.execute('UPDATE interactions SET saved = 0 WHERE user_id = ? AND video_id = ?', (user_id, video_id)) db.execute('UPDATE videos SET saves_count = saves_count - 1 WHERE id = ?', (video_id,)) saved_status = False else: - db.execute('INSERT INTO saved_videos (user_id, video_id) VALUES (?, ?)', (user_id, video_id)) + db.execute(''' + INSERT INTO interactions (user_id, video_id, saved, watched) + VALUES (?, ?, 1, 1) + ON CONFLICT(user_id, video_id) DO UPDATE SET saved = 1 + ''', (user_id, video_id)) db.execute('UPDATE videos SET saves_count = saves_count + 1 WHERE id = ?', (video_id,)) saved_status = True - db.commit() - saves = db.execute('SELECT saves_count FROM videos WHERE id = ?', (video_id,)).fetchone()['saves_count'] - - track_event(user_id, 'save', {'video_id': video_id, 'saved': saved_status}) - return jsonify({'status': 'ok', 'saved': saved_status, 'saves_count': saves}) - except Exception as e: logger.error(f"Error in save_video: {e}") return jsonify({'error': str(e)}), 500 -@app.route('/api/comments/', methods=['GET', 'POST']) +@app.route('/api/saved') @login_required -def api_comments(video_id): - if request.method == 'GET': - try: - db = get_db() - - comments = db.execute(''' +def get_saved_videos(): + try: + db = get_db() + videos = db.execute(''' + SELECT v.*, u.username, u.avatar + FROM interactions i + JOIN videos v ON i.video_id = v.id + JOIN users u ON v.user_id = u.id + WHERE i.user_id = ? AND i.saved = 1 + ORDER BY i.timestamp DESC + ''', (session['user_id'],)).fetchall() + result = [] + for v in videos: + video = dict(v) + video['url'] = v['cloudinary_url'] + video['avatar_url'] = f'/avatars/{v["avatar"]}' + video.pop('vector', None) + result.append(video) + return jsonify(result) + except Exception as e: + logger.error(f"Error in get_saved_videos: {e}") + return jsonify([]), 500 + +# ---------- نظام التعليقات (مختصر) ---------- +@app.route('/api/comments/') +@login_required +def get_comments(video_id): + try: + db = get_db() + comments = db.execute(''' + SELECT c.*, u.username, u.avatar, u.is_verified, + (SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count, + (SELECT COUNT(*) FROM comments WHERE parent_id = c.id) as replies_count + FROM comments c + JOIN users u ON c.user_id = u.id + WHERE c.video_id = ? AND c.parent_id IS NULL + ORDER BY c.is_pinned DESC, c.likes_count DESC, c.timestamp DESC + LIMIT 50 + ''', (video_id,)).fetchall() + result = [] + for comment in comments: + c = dict(comment) + replies = db.execute(''' SELECT c.*, u.username, u.avatar, u.is_verified, - (SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count, - (SELECT COUNT(*) FROM comments WHERE parent_id = c.id) as replies_count + (SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count FROM comments c JOIN users u ON c.user_id = u.id - WHERE c.video_id = ? AND c.parent_id IS NULL - ORDER BY c.is_pinned DESC, c.likes_count DESC, c.timestamp DESC - LIMIT 50 - ''', (video_id,)).fetchall() - - result = [] - for comment in comments: - c = dict(comment) - - replies = db.execute(''' - SELECT c.*, u.username, u.avatar, u.is_verified, - (SELECT COUNT(*) FROM comment_likes WHERE comment_id = c.id) as likes_count - FROM comments c - JOIN users u ON c.user_id = u.id - WHERE c.parent_id = ? - ORDER BY c.timestamp ASC - LIMIT 10 - ''', (comment['id'],)).fetchall() - - c['replies'] = [] - for r in replies: - reply = dict(r) - liked = db.execute('SELECT * FROM comment_likes WHERE user_id = ? AND comment_id = ?', - (session['user_id'], r['id'])).fetchone() - reply['liked_by_user'] = liked is not None - c['replies'].append(reply) - + WHERE c.parent_id = ? + ORDER BY c.timestamp ASC + LIMIT 10 + ''', (comment['id'],)).fetchall() + c['replies'] = [] + for r in replies: + reply = dict(r) liked = db.execute('SELECT * FROM comment_likes WHERE user_id = ? AND comment_id = ?', - (session['user_id'], comment['id'])).fetchone() - c['liked_by_user'] = liked is not None - result.append(c) - - return jsonify(result) - - except Exception as e: - logger.error(f"Error in get_comments: {e}") - return jsonify([]), 500 - - else: # POST - try: - user_id = session['user_id'] - data = request.get_json() - comment_text = data.get('comment', '').strip() - parent_id = data.get('parent_id') - - if not comment_text: - return jsonify({'error': 'التعليق لا يمكن أن يكون فارغاً'}), 400 - - if len(comment_text) > 500: - return jsonify({'error': 'التعليق طويل جداً'}), 400 - - db = get_db() - cursor = db.execute(''' - INSERT INTO comments (user_id, video_id, parent_id, comment_text) - VALUES (?, ?, ?, ?) - ''', (user_id, video_id, parent_id, comment_text)) - - comment_id = cursor.lastrowid - db.execute('UPDATE videos SET comments_count = comments_count + 1 WHERE id = ?', (video_id,)) - - video = db.execute('SELECT user_id FROM videos WHERE id = ?', (video_id,)).fetchone() - if video and video['user_id'] != user_id: + (session['user_id'], r['id'])).fetchone() + reply['liked_by_user'] = liked is not None + reply['avatar_url'] = f'/avatars/{r["avatar"]}' + c['replies'].append(reply) + liked = db.execute('SELECT * FROM comment_likes WHERE user_id = ? AND comment_id = ?', + (session['user_id'], comment['id'])).fetchone() + c['liked_by_user'] = liked is not None + c['avatar_url'] = f'/avatars/{comment["avatar"]}' + result.append(c) + return jsonify(result) + except Exception as e: + logger.error(f"Error in get_comments: {e}") + return jsonify([]), 500 + +@app.route('/api/comment/', methods=['POST']) +@login_required +def add_comment(video_id): + try: + user_id = session['user_id'] + data = request.get_json() + comment_text = data.get('comment', '').strip() + parent_id = data.get('parent_id') + if not comment_text: + return jsonify({'error': 'التعليق لا يمكن أن يكون فارغاً'}), 400 + if len(comment_text) > 500: + return jsonify({'error': 'التعليق طويل جداً'}), 400 + db = get_db() + cursor = db.execute(''' + INSERT INTO comments (user_id, video_id, parent_id, comment_text) + VALUES (?, ?, ?, ?) + ''', (user_id, video_id, parent_id, comment_text)) + comment_id = cursor.lastrowid + db.execute('UPDATE videos SET comments_count = comments_count + 1 WHERE id = ?', (video_id,)) + video = db.execute('SELECT user_id FROM videos WHERE id = ?', (video_id,)).fetchone() + if video and video['user_id'] != user_id: + user = db.execute('SELECT username FROM users WHERE id = ?', (user_id,)).fetchone() + if user: + add_notification(video['user_id'], user_id, 'comment', f'علق {user["username"]} على فيديو لك', video_id, comment_id) + if parent_id: + parent = db.execute('SELECT user_id FROM comments WHERE id = ?', (parent_id,)).fetchone() + if parent and parent['user_id'] != user_id: user = db.execute('SELECT username FROM users WHERE id = ?', (user_id,)).fetchone() if user: - add_notification(video['user_id'], user_id, 'comment', f'علق {user["username"]} على فيديو لك', video_id, comment_id) - - if parent_id: - parent = db.execute('SELECT user_id FROM comments WHERE id = ?', (parent_id,)).fetchone() - if parent and parent['user_id'] != user_id: - user = db.execute('SELECT username FROM users WHERE id = ?', (user_id,)).fetchone() - if user: - add_notification(parent['user_id'], user_id, 'reply', f'رد {user["username"]} على تعليقك', video_id, comment_id) - - db.commit() - - add_xp(user_id, 'comment', 3) - track_event(user_id, 'add_comment', {'video_id': video_id, 'comment_id': comment_id}) - - return jsonify({'status': 'ok', 'comment_id': comment_id}) - - except Exception as e: - logger.error(f"Error in add_comment: {e}") - return jsonify({'error': str(e)}), 500 + add_notification(parent['user_id'], user_id, 'reply', f'رد {user["username"]} على تعليقك', video_id, comment_id) + db.commit() + add_xp(user_id, 'comment', 3) + return jsonify({'status': 'ok', 'comment_id': comment_id}) + except Exception as e: + logger.error(f"Error in add_comment: {e}") + return jsonify({'error': str(e)}), 500 @app.route('/api/comment/like/', methods=['POST']) @login_required -def api_comment_like(comment_id): +def like_comment(comment_id): try: user_id = session['user_id'] db = get_db() - liked = db.execute('SELECT * FROM comment_likes WHERE user_id = ? AND comment_id = ?', (user_id, comment_id)).fetchone() - if liked: db.execute('DELETE FROM comment_likes WHERE user_id = ? AND comment_id = ?', (user_id, comment_id)) db.execute('UPDATE comments SET likes_count = likes_count - 1 WHERE id = ?', (comment_id,)) @@ -1834,310 +1330,120 @@ def api_comment_like(comment_id): db.execute('INSERT INTO comment_likes (user_id, comment_id) VALUES (?, ?)', (user_id, comment_id)) db.execute('UPDATE comments SET likes_count = likes_count + 1 WHERE id = ?', (comment_id,)) liked_status = True - comment = db.execute('SELECT user_id FROM comments WHERE id = ?', (comment_id,)).fetchone() if comment and comment['user_id'] != user_id: user = db.execute('SELECT username FROM users WHERE id = ?', (user_id,)).fetchone() if user: add_notification(comment['user_id'], user_id, 'like_comment', f'أعجب {user["username"]} بتعليقك') - db.commit() - likes_count = db.execute('SELECT likes_count FROM comments WHERE id = ?', (comment_id,)).fetchone()['likes_count'] - - track_event(user_id, 'like_comment', {'comment_id': comment_id, 'liked': liked_status}) - return jsonify({'status': 'ok', 'liked': liked_status, 'likes_count': likes_count}) - except Exception as e: logger.error(f"Error in like_comment: {e}") return jsonify({'error': str(e)}), 500 -# مسارات الإشعارات -@app.route('/api/notifications/stream') -@login_required -def notification_stream(): - user_id = session['user_id'] - - def event_stream(): - q = queue.Queue() - notification_queues[user_id] = q - - try: - while True: - try: - notification = q.get(timeout=30) - yield f"data: {json.dumps(notification, ensure_ascii=False)}\n\n" - except queue.Empty: - yield "data: {}\n\n".format(json.dumps({'type': 'ping'}, ensure_ascii=False)) - except GeneratorExit: - notification_queues.pop(user_id, None) - - return Response(event_stream(), mimetype="text/event-stream", - headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) - -@app.route('/api/notifications') +# ---------- مسارات إضافية: الكاميرا والتسجيل ---------- +@app.route('/api/camera/upload', methods=['POST']) @login_required -def get_notifications(): - try: - db = get_db() - - notifications = db.execute(''' - SELECT n.*, u.username, u.avatar - FROM notifications n - LEFT JOIN users u ON n.from_user_id = u.id - WHERE n.user_id = ? - ORDER BY n.created_at DESC - LIMIT 50 - ''', (session['user_id'],)).fetchall() - - db.execute('UPDATE notifications SET is_read = 1 WHERE user_id = ?', (session['user_id'],)) - db.commit() - - result = [] - for n in notifications: - notif = dict(n) - notif['avatar_url'] = f"/avatars/{n['avatar']}" if n['avatar'] else '/avatars/default.jpg' - result.append(notif) - - return jsonify(result) - - except Exception as e: - logger.error(f"Error in get_notifications: {e}") - return jsonify([]), 500 +def upload_recorded_video(): + if 'video' not in request.files: + return jsonify({'error': 'لا يوجد فيديو'}), 400 + file = request.files['video'] + if file.filename == '': + return jsonify({'error': 'اسم ملف فارغ'}), 400 + temp_path = os.path.join('/tmp', f"rec_{uuid.uuid4().hex}.mp4") + file.save(temp_path) + cloudinary_url, public_id = upload_video_to_cloudinary(temp_path) + os.remove(temp_path) + if not cloudinary_url: + return jsonify({'error': 'فشل الرفع'}), 500 + db = get_db() + cursor = db.execute(''' + INSERT INTO videos (user_id, cloudinary_url, cloudinary_public_id, title, description, visibility) + VALUES (?, ?, ?, ?, ?, ?) + ''', (session['user_id'], cloudinary_url, public_id, 'تسجيل كاميرا', '', 'public')) + video_id = cursor.lastrowid + db.commit() + add_xp(session['user_id'], 'upload_video', 20) + return jsonify({'status': 'ok', 'video_id': video_id, 'url': cloudinary_url}) -@app.route('/api/notifications/count') +# ---------- مسار العلامة المائية ---------- +@app.route('/api/watermark/', methods=['POST']) @login_required -def notification_count(): +def watermark_video(video_id): try: db = get_db() - count = db.execute('SELECT COUNT(*) FROM notifications WHERE user_id = ? AND is_read = 0', - (session['user_id'],)).fetchone()[0] - - return jsonify({'count': count}) - + video = db.execute('SELECT cloudinary_public_id FROM videos WHERE id = ?', (video_id,)).fetchone() + if not video: + return jsonify({'error': 'فيديو غير موجود'}), 404 + public_id = video['cloudinary_public_id'] + watermarked_url = add_watermark_to_cloudinary_video(public_id, text="ARC") + return jsonify({'watermarked_url': watermarked_url}) except Exception as e: - logger.error(f"Error in notification_count: {e}") - return jsonify({'count': 0}), 500 - -# مسارات التحليلات -@app.route('/api/analytics/stats', methods=['GET']) -@login_required -def analytics_stats(): - stats = get_user_stats(session['user_id']) - return jsonify(stats) - -@app.route('/api/analytics/track', methods=['POST']) -@login_required -def track_analytics(): - data = request.get_json() - event_name = data.get('event_name') - event_data = data.get('event_data') - - if not event_name: - return jsonify({'error': 'event_name مطلوب'}), 400 - - track_event(session['user_id'], event_name, event_data) - return jsonify({'status': 'ok'}) - -# مسارات التسويق بالعمولة -@app.route('/api/affiliate/link', methods=['GET']) -@login_required -def get_affiliate_link(): - link = generate_affiliate_link(session['user_id']) - return jsonify({'link': request.host_url.rstrip('/') + link}) + logger.error(f"Watermark error: {e}") + return jsonify({'error': str(e)}), 500 -@app.route('/api/affiliate/stats', methods=['GET']) +# ---------- مسارات API للبروفايل ---------- +@app.route('/api/profile/') @login_required -def affiliate_stats(): +def api_profile(user_id): db = get_db() user = db.execute(''' - SELECT affiliate_clicks, affiliate_conversions, affiliate_balance - FROM users WHERE id = ? - ''', (session['user_id'],)).fetchone() - - clicks = db.execute(''' - SELECT clicked_at, converted FROM affiliate_clicks - WHERE referrer_id = ? ORDER BY clicked_at DESC LIMIT 50 - ''', (session['user_id'],)).fetchall() - + SELECT u.*, s.* + FROM users u + LEFT JOIN user_settings s ON u.id = s.user_id + WHERE u.id = ? + ''', (user_id,)).fetchone() + if not user: + return jsonify({'error': 'المستخدم غير موجود'}), 404 + videos = db.execute(''' + SELECT id, title, thumbnail_url, cloudinary_url, views, likes_count, comments_count, upload_time + FROM videos WHERE user_id = ? AND visibility = 'public' + ORDER BY upload_time DESC LIMIT 30 + ''', (user_id,)).fetchall() + followers_count = db.execute('SELECT COUNT(*) FROM follows WHERE user_id = ?', (user_id,)).fetchone()[0] + following_count = db.execute('SELECT COUNT(*) FROM follows WHERE follower_id = ?', (user_id,)).fetchone()[0] + is_following = False + if session['user_id'] != user_id: + is_following = db.execute('SELECT * FROM follows WHERE user_id = ? AND follower_id = ?', + (user_id, session['user_id'])).fetchone() is not None + total_views = db.execute('SELECT SUM(views) FROM videos WHERE user_id = ?', (user_id,)).fetchone()[0] or 0 return jsonify({ - 'clicks': user['affiliate_clicks'], - 'conversions': user['affiliate_conversions'], - 'balance': user['affiliate_balance'], - 'recent_clicks': [dict(c) for c in clicks] + 'user': dict(user), + 'videos': [dict(v) for v in videos], + 'followers_count': followers_count, + 'following_count': following_count, + 'is_following': is_following, + 'total_views': total_views }) -@app.route('/r/') -def referral_redirect(referral_code): - ip = request.remote_addr - user_agent = request.headers.get('User-Agent', '') - track_affiliate_click(referral_code, ip, user_agent) - return redirect(url_for('register', ref=referral_code)) - -# مسارات العملات والمكافآت -@app.route('/api/coins/balance') -@login_required -def coins_balance(): - try: - db = get_db() - user = db.execute('SELECT coins, diamonds FROM users WHERE id = ?', (session['user_id'],)).fetchone() - return jsonify({'coins': user['coins'], 'diamonds': user['diamonds']}) - - except Exception as e: - logger.error(f"Error in coins_balance: {e}") - return jsonify({'coins': 0, 'diamonds': 0}), 500 - -@app.route('/api/transactions') +# ---------- مسارات الطبقة الزجاجية ---------- +@app.route('/glass/profile/') @login_required -def get_transactions(): - try: - db = get_db() - transactions = db.execute(''' - SELECT * FROM transactions - WHERE user_id = ? - ORDER BY created_at DESC - LIMIT 100 - ''', (session['user_id'],)).fetchall() - - return jsonify([dict(t) for t in transactions]) - - except Exception as e: - logger.error(f"Error in get_transactions: {e}") - return jsonify([]), 500 +def glass_profile(user_id): + return render_template_string(GLASS_PROFILE_TEMPLATE, user_id=user_id) -@app.route('/api/gamification/daily', methods=['POST']) -@login_required -def claim_daily(): - success, *rest = claim_daily_reward(session['user_id']) - - if success: - coins, streak = rest - track_event(session['user_id'], 'claim_daily', {'coins': coins, 'streak': streak}) - return jsonify({'status': 'ok', 'coins': coins, 'streak': streak}) - else: - return jsonify({'error': rest[0]}), 400 - -@app.route('/api/gamification/leaderboard', methods=['GET']) +@app.route('/glass/saved') @login_required -def leaderboard(): - limit = int(request.args.get('limit', 50)) - - db = get_db() - users = db.execute(''' - SELECT username, avatar, xp, level, is_verified - FROM users - ORDER BY xp DESC - LIMIT ? - ''', (limit,)).fetchall() - - result = [] - for u in users: - user = dict(u) - user['avatar_url'] = f"/avatars/{u['avatar']}" - result.append(user) - - return jsonify(result) +def glass_saved(): + return render_template_string(GLASS_SAVED_TEMPLATE) -# مسارات المطور -@app.route('/developer') -@developer_required -def developer_panel(): - return render_template_string(DEVELOPER_TEMPLATE) - -@app.route('/api/developer/session/create', methods=['POST']) +@app.route('/glass/friends') @login_required -def create_dev_session(): - if not session.get('is_developer'): - return jsonify({'error': 'غير مصرح'}), 403 - - token = create_developer_session(session['user_id']) - return jsonify({'session_token': token}) +def glass_friends(): + return render_template_string(GLASS_FRIENDS_TEMPLATE) -@app.route('/api/developer/execute', methods=['POST']) +@app.route('/glass/search') @login_required -def execute_code_route(): - if not session.get('is_developer'): - return jsonify({'error': 'غير مصرح'}), 403 - - data = request.get_json() - code = data.get('code', '') - - token = request.headers.get('X-Dev-Token') - if token: - uid = validate_developer_session(token) - if uid != session['user_id']: - return jsonify({'error': 'جلسة مطور غير صالحة'}), 401 - - result = execute_code_in_sandbox(code) - - track_event(session['user_id'], 'execute_code', {'code_length': len(code)}) - - return jsonify(result) - -@app.route('/api/developer/plugins', methods=['GET']) -@developer_required -def list_plugins(): - db = get_db() - plugins = db.execute('SELECT id, name, is_active, created_at FROM developer_plugins WHERE user_id = ?', - (session['user_id'],)).fetchall() - - return jsonify([dict(p) for p in plugins]) - -@app.route('/api/developer/plugins/upload', methods=['POST']) -@developer_required -def upload_plugin_route(): - if 'plugin' not in request.files: - return jsonify({'error': 'لم يتم اختيار ملف'}), 400 - - file = request.files['plugin'] - if file.filename == '': - return jsonify({'error': 'لم يتم اختيار ملف'}), 400 - - if not allowed_plugin(file.filename): - return jsonify({'error': 'نوع الملف غير مدعوم'}), 400 - - name = request.form.get('name', '').strip() - if not name: - name = os.path.splitext(file.filename)[0] - - code = file.read().decode('utf-8') - - success = install_plugin(session['user_id'], name, code) - - if success: - track_event(session['user_id'], 'upload_plugin', {'plugin_name': name}) - return jsonify({'status': 'ok', 'name': name}) - else: - return jsonify({'error': 'فشل تثبيت الإضافة'}), 500 - -@app.route('/api/developer/plugins/toggle/', methods=['POST']) -@developer_required -def toggle_plugin_route(plugin_id): - data = request.get_json() - is_active = data.get('is_active', True) - - db = get_db() - db.execute('UPDATE developer_plugins SET is_active = ? WHERE id = ? AND user_id = ?', - (is_active, plugin_id, session['user_id'])) - db.commit() - - load_all_plugins() - - track_event(session['user_id'], 'toggle_plugin', {'plugin_id': plugin_id, 'is_active': is_active}) - - return jsonify({'status': 'ok'}) +def glass_search(): + return render_template_string(GLASS_SEARCH_TEMPLATE) -# مسارات الملفات الثابتة +# ---------- دوال الملفات (للصور الشخصية) ---------- @app.route('/avatars/') def serve_avatar(filename): - return send_from_directory(AVATAR_FOLDER, filename) - -@app.route('/thumbnails/') -def serve_thumbnail(filename): - return send_from_directory(THUMBNAIL_FOLDER, filename) + return send_from_directory(app.config['AVATAR_FOLDER'], filename) -# قوالب HTML +# ---------- قوالب HTML (معاد تصميمها) ---------- LOGIN_PAGE = ''' @@ -2148,44 +1454,44 @@ LOGIN_PAGE = ''' -