diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,93 +1,69 @@ import os -import sys import uuid import re import sqlite3 import numpy as np from datetime import datetime, timedelta import random -import string import hashlib import json -import time -import threading -import queue import logging import secrets -import base64 from functools import wraps -from contextlib import contextmanager -# استيراد Cloudinary +# Cloudinary import cloudinary import cloudinary.uploader import cloudinary.api -# إنشاء المجلدات الضرورية (للصور الشخصية فقط) -os.makedirs('logs', exist_ok=True) -os.makedirs('avatars', exist_ok=True) -os.makedirs('thumbnails', exist_ok=True) - -# إعداد التسجيل -logging.basicConfig( - filename='logs/app.log', - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +# Flask +from flask import ( + Flask, request, session, g, jsonify, render_template_string, + redirect, url_for, abort, send_from_directory ) -logger = logging.getLogger(__name__) +from werkzeug.utils import secure_filename +from werkzeug.security import generate_password_hash, check_password_hash -# محاولة استيراد المكتبات مع fallback +# Optional libraries try: - from PIL import Image, ImageDraw, ImageFont + from PIL import Image, ImageDraw HAS_PIL = True except ImportError: HAS_PIL = False - logger.warning("PIL غير مثبت، ميزات الصور محدودة.") try: import cv2 + import numpy as np HAS_CV2 = True except ImportError: HAS_CV2 = False - logger.warning("OpenCV غير مثبت، ميزات كشف NSFW وتحليل الفيديو غير متاحة.") -from flask import ( - Flask, request, session, g, jsonify, render_template_string, - redirect, url_for, abort, send_from_directory, Response -) -from werkzeug.utils import secure_filename -from werkzeug.security import generate_password_hash, check_password_hash +# ---------- Configuration ---------- +app = Flask(__name__) +app.secret_key = secrets.token_hex(32) +app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) +app.config['JSON_AS_ASCII'] = False -# ---------- تكوين Cloudinary ---------- +# Folders +AVATAR_FOLDER = 'avatars' +os.makedirs(AVATAR_FOLDER, exist_ok=True) + +# 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 +# Database +DATABASE = 'arc_video.db' +VECTOR_DIM = 10 -# إعدادات رفع الملفات (للصور الشخصية فقط) -AVATAR_FOLDER = os.path.join(os.getcwd(), 'avatars') -THUMBNAIL_FOLDER = os.path.join(os.getcwd(), 'thumbnails') +# Allowed extensions ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm', '3gp'} ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} -app.config['AVATAR_FOLDER'] = AVATAR_FOLDER -app.config['THUMBNAIL_FOLDER'] = THUMBNAIL_FOLDER -app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 - -# قاعدة البيانات -DATABASE = os.path.join(os.getcwd(), 'arc_video.db') -VECTOR_DIM = 10 -notification_queues = {} - -# ---------- دوال قاعدة البيانات ---------- +# ---------- Database helpers ---------- def get_db(): db = getattr(g, '_database', None) if db is None: @@ -102,12 +78,11 @@ def close_connection(exception): db.close() def init_db(): - """إنشاء قاعدة البيانات وجميع الجداول""" with app.app_context(): db = get_db() cursor = db.cursor() - - # جدول المستخدمين + + # Users cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -117,58 +92,29 @@ def init_db(): avatar TEXT DEFAULT 'default.jpg', cover_image TEXT DEFAULT 'default_cover.jpg', bio TEXT, - phone TEXT, - gender TEXT, - country TEXT, - birth_date DATE, is_verified BOOLEAN DEFAULT 0, is_admin BOOLEAN DEFAULT 0, coins INTEGER DEFAULT 100, - diamonds INTEGER DEFAULT 0, referral_code TEXT UNIQUE, - referred_by INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP, xp INTEGER DEFAULT 0, - level INTEGER DEFAULT 1, - affiliate_balance INTEGER DEFAULT 0, - affiliate_clicks INTEGER DEFAULT 0, - affiliate_conversions INTEGER DEFAULT 0, - is_developer BOOLEAN DEFAULT 0, - developer_api_key TEXT, - FOREIGN KEY(referred_by) REFERENCES users(id) + level INTEGER DEFAULT 1 ) ''') - - # جدول إعدادات المستخدم - cursor.execute(''' - CREATE TABLE IF NOT EXISTS user_settings ( - user_id INTEGER PRIMARY KEY, - privacy_profile TEXT DEFAULT 'public', - privacy_videos TEXT DEFAULT 'public', - notifications_likes BOOLEAN DEFAULT 1, - notifications_comments BOOLEAN DEFAULT 1, - notifications_follows BOOLEAN DEFAULT 1, - notifications_messages BOOLEAN DEFAULT 1, - dark_mode BOOLEAN DEFAULT 1, - language TEXT DEFAULT 'ar', - FOREIGN KEY(user_id) REFERENCES users(id) - ) - ''') - - # جدول المتابعات (الأصدقاء) + + # Follows cursor.execute(''' CREATE TABLE IF NOT EXISTS follows ( user_id INTEGER, follower_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, follower_id), FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(follower_id) REFERENCES users(id), - PRIMARY KEY (user_id, follower_id) + FOREIGN KEY(follower_id) REFERENCES users(id) ) ''') - - # جدول الفيديوهات مع Cloudinary + + # Videos (Cloudinary) cursor.execute(''' CREATE TABLE IF NOT EXISTS videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -178,11 +124,7 @@ def init_db(): thumbnail_url TEXT, title TEXT, description TEXT, - duration INTEGER DEFAULT 0, - music TEXT, allow_comments BOOLEAN DEFAULT 1, - allow_duet BOOLEAN DEFAULT 1, - allow_stitch BOOLEAN DEFAULT 1, visibility TEXT DEFAULT 'public', upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, views INTEGER DEFAULT 0, @@ -191,38 +133,30 @@ def init_db(): saves_count INTEGER DEFAULT 0, comments_count INTEGER DEFAULT 0, vector BLOB, - challenge_id INTEGER, - is_reported BOOLEAN DEFAULT 0, - report_count INTEGER DEFAULT 0, nsfw_score REAL DEFAULT 0.0, ai_tags TEXT, - processed_for_ai BOOLEAN DEFAULT 0, - FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(challenge_id) REFERENCES challenges(id) + FOREIGN KEY(user_id) REFERENCES users(id) ) ''') - - # جدول التفاعلات + + # Interactions cursor.execute(''' CREATE TABLE IF NOT EXISTS interactions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, video_id INTEGER NOT NULL, liked BOOLEAN DEFAULT 0, watched BOOLEAN DEFAULT 0, - shared BOOLEAN DEFAULT 0, saved BOOLEAN DEFAULT 0, - reported BOOLEAN DEFAULT 0, + shared BOOLEAN DEFAULT 0, watch_time INTEGER DEFAULT 0, - voted_challenge BOOLEAN DEFAULT 0, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, video_id), FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(video_id) REFERENCES videos(id), - UNIQUE(user_id, video_id) + FOREIGN KEY(video_id) REFERENCES videos(id) ) ''') - - # جدول التعليقات + + # Comments cursor.execute(''' CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -231,17 +165,13 @@ def init_db(): parent_id INTEGER, comment_text TEXT NOT NULL, likes_count INTEGER DEFAULT 0, - is_pinned BOOLEAN DEFAULT 0, - is_reported BOOLEAN DEFAULT 0, - report_count INTEGER DEFAULT 0, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(video_id) REFERENCES videos(id), FOREIGN KEY(parent_id) REFERENCES comments(id) ) ''') - - # جدول إعجابات التعليقات + cursor.execute(''' CREATE TABLE IF NOT EXISTS comment_likes ( user_id INTEGER NOT NULL, @@ -251,579 +181,268 @@ def init_db(): FOREIGN KEY(comment_id) REFERENCES comments(id) ) ''') - - # جدول الهاشتاغات + + # Hashtags cursor.execute(''' CREATE TABLE IF NOT EXISTS hashtags ( id INTEGER PRIMARY KEY AUTOINCREMENT, tag TEXT UNIQUE NOT NULL, - usage_count INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + usage_count INTEGER DEFAULT 1 ) ''') - - # جدول ربط الفيديوهات بالهاشتاغات + cursor.execute(''' CREATE TABLE IF NOT EXISTS video_hashtags ( video_id INTEGER NOT NULL, hashtag_id INTEGER NOT NULL, + PRIMARY KEY (video_id, hashtag_id), FOREIGN KEY(video_id) REFERENCES videos(id), - FOREIGN KEY(hashtag_id) REFERENCES hashtags(id), - PRIMARY KEY (video_id, hashtag_id) + FOREIGN KEY(hashtag_id) REFERENCES hashtags(id) ) ''') - - # جدول الإشعارات + + # Notifications (simplified) cursor.execute(''' CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, from_user_id INTEGER, - type TEXT NOT NULL, + type TEXT, content TEXT, video_id INTEGER, - comment_id INTEGER, is_read BOOLEAN DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(user_id) REFERENCES users(id), - FOREIGN KEY(from_user_id) REFERENCES users(id), - FOREIGN KEY(video_id) REFERENCES videos(id), - FOREIGN KEY(comment_id) REFERENCES comments(id) - ) - ''') - - # جدول الإحالات - cursor.execute(''' - CREATE TABLE IF NOT EXISTS referrals ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - referrer_id INTEGER NOT NULL, - referred_id INTEGER NOT NULL, - reward_coins INTEGER DEFAULT 50, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(referrer_id) REFERENCES users(id), - FOREIGN KEY(referred_id) REFERENCES users(id), - UNIQUE(referred_id) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') - - # جدول نقاط الخبرة + + # XP cursor.execute(''' CREATE TABLE IF NOT EXISTS user_xp ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, action TEXT NOT NULL, xp_gained INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(user_id) REFERENCES users(id) - ) - ''') - - # جدول المكافآت اليومية - cursor.execute(''' - CREATE TABLE IF NOT EXISTS daily_rewards ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - reward_date DATE NOT NULL, - reward_coins INTEGER NOT NULL, - streak INTEGER DEFAULT 1, - claimed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, reward_date) - ) - ''') - - # جدول التحديات (مختصر) - 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) - ) - ''') - + db.commit() - - # إنشاء المستخدمين الافتراضيين + + # Create default users create_default_users() create_default_avatars() -# ---------- دوال مساعدة ---------- -def generate_referral_code(): - return secrets.token_hex(4).upper() - -def random_vector(dim=VECTOR_DIM): - vec = np.random.randn(dim).astype(np.float32) - return vec.tobytes() - -def bytes_to_vector(b): - if b is None: - return np.zeros(VECTOR_DIM, dtype=np.float32) - return np.frombuffer(b, dtype=np.float32) - -def allowed_video(filename): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_VIDEO_EXTENSIONS - -def allowed_image(filename): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS - def create_default_users(): db = get_db() - admin = db.execute("SELECT * FROM users WHERE username = 'admin'").fetchone() - if not admin: + if not db.execute("SELECT * FROM users WHERE username='admin'").fetchone(): hashed = generate_password_hash('admin123') - referral = generate_referral_code() - db.execute(''' - 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,)) + code = secrets.token_hex(4).upper() + db.execute("INSERT INTO users (username, email, password_hash, is_admin, referral_code) VALUES (?,?,?,?,?)", + ('admin', 'admin@arc.com', hashed, 1, code)) + uid = db.execute("SELECT last_insert_rowid()").fetchone()[0] db.commit() - - test = db.execute("SELECT * FROM users WHERE username = 'test'").fetchone() - if not test: + + if not db.execute("SELECT * FROM users WHERE username='test'").fetchone(): hashed = generate_password_hash('test123') - referral = generate_referral_code() - db.execute(''' - INSERT INTO users (username, email, password_hash, referral_code) - VALUES (?, ?, ?, ?) - ''', ('test', 'test@arc.com', hashed, referral)) - test_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] - db.execute('INSERT INTO user_settings (user_id) VALUES (?)', (test_id,)) + code = secrets.token_hex(4).upper() + db.execute("INSERT INTO users (username, email, password_hash, referral_code) VALUES (?,?,?,?)", + ('test', 'test@arc.com', hashed, code)) 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 not os.path.exists(default_avatar_path): + default_avatar = os.path.join(AVATAR_FOLDER, 'default.jpg') + default_cover = os.path.join(AVATAR_FOLDER, 'default_cover.jpg') + if not os.path.exists(default_avatar): if HAS_PIL: - img = Image.new('RGB', (200, 200), color=(255, 45, 85)) + 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) + d.ellipse((20,20,180,180), fill=(255,255,255)) + d.text((70,90), "ARC", fill=(255,45,85)) + img.save(default_avatar) else: - with open(default_avatar_path, 'w') as f: - f.write('default avatar placeholder') - if not os.path.exists(default_cover_path): + with open(default_avatar, 'w') as f: f.write('avatar') + if not os.path.exists(default_cover): 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) + img = Image.new('RGB', (800,200), color=(30,30,30)) + d = ImageDraw.Draw(img) + d.text((350,80), "ARC VIDEO", fill=(255,45,85)) + img.save(default_cover) else: - with open(default_cover_path, 'w') as f: - f.write('default cover placeholder') + with open(default_cover, 'w') as f: f.write('cover') -# ---------- دوال 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) +# ---------- Utility functions ---------- +def allowed_video(filename): + return '.' in filename and filename.rsplit('.',1)[1].lower() in ALLOWED_VIDEO_EXTENSIONS -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} - ] - ) - return transformed_url +def allowed_image(filename): + return '.' in filename and filename.rsplit('.',1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS -# ---------- دوال الذكاء الاصطناعي (تبقى كما هي) ---------- -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) - ret, frame = cap.read() - if ret: - hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) - lower_skin = np.array([0, 20, 70], dtype=np.uint8) - upper_skin = np.array([20, 255, 255], dtype=np.uint8) - mask = cv2.inRange(hsv, lower_skin, upper_skin) - 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 random_vector(dim=VECTOR_DIM): + return np.random.randn(dim).astype(np.float32).tobytes() -def extract_video_features(video_path): +def extract_video_features(path): if not HAS_CV2: return random_vector() try: - cap = cv2.VideoCapture(video_path) + cap = cv2.VideoCapture(path) 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 + resized = cv2.resize(gray, (32,32)) + features = resized.flatten().astype(np.float32) / 255.0 if len(features) > VECTOR_DIM: - step = len(features) // VECTOR_DIM + step = len(features)//VECTOR_DIM features = features[::step][:VECTOR_DIM] - elif len(features) < VECTOR_DIM: + else: 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}") + except: 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) - return json.dumps(list(tags)[:15]) - -# ---------- دوال نقاط الخبرة والمكافآت ---------- -def add_xp(user_id, action, xp_amount): - db = get_db() +def analyze_nsfw(path): + if not HAS_CV2: + return 0.0 try: - 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 = 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: - logger.error(f"Error adding XP: {e}") - return False + cap = cv2.VideoCapture(path) + total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + if total == 0: + return 0.0 + samples = min(10, total) + indices = [int(i*total/samples) for i in range(samples)] + skin = 0 + for idx in indices: + cap.set(cv2.CAP_PROP_POS_FRAMES, idx) + ret, frame = cap.read() + if ret: + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + lower = np.array([0,20,70]) + upper = np.array([20,255,255]) + mask = cv2.inRange(hsv, lower, upper) + perc = cv2.countNonZero(mask) / (frame.shape[0]*frame.shape[1]) * 100 + if perc > 30: + skin += 1 + cap.release() + return skin / samples + except: + return 0.0 -def claim_daily_reward(user_id): - db = get_db() - today = datetime.now().date() - try: - 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() - 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('UPDATE users SET coins = coins + ? WHERE id = ?', (coins, user_id)) - db.commit() - return True, coins, streak - except Exception as e: - logger.error(f"Error claiming daily reward: {e}") - return False, str(e) +def generate_auto_tags(path, title, desc): + tags = set() + text = f"{title} {desc}" + for word in re.findall(r'#(\w+)', text): + tags.add(word) + for word in re.findall(r'[\w]+', text): + if len(word) > 2 and word.isalpha(): + tags.add(word.lower()) + return json.dumps(list(tags)[:15]) -# ---------- دوال التسويق بالعمولة ---------- -def generate_affiliate_link(user_id): - code = secrets.token_urlsafe(8) +def add_xp(user_id, action, amount): db = get_db() - db.execute('UPDATE users SET referral_code = ? WHERE id = ?', (code, user_id)) + db.execute("INSERT INTO user_xp (user_id, action, xp_gained) VALUES (?,?,?)", + (user_id, action, amount)) + db.execute("UPDATE users SET xp = xp + ? WHERE id = ?", (amount, user_id)) + user = db.execute("SELECT xp FROM users WHERE id=?", (user_id,)).fetchone() + if user: + new_level = int((user['xp'] // 100) ** 0.5) + 1 + db.execute("UPDATE users SET level = ? WHERE id=?", (new_level, user_id)) db.commit() - return f"/register?ref={code}" - -def track_affiliate_click(referral_code, ip, user_agent): - db = get_db() - try: - 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('UPDATE users SET affiliate_clicks = affiliate_clicks + 1 WHERE id = ?', (referrer['id'],)) - db.commit() - return True - except Exception as e: - logger.error(f"Error tracking affiliate click: {e}") - return False - -def process_affiliate_conversion(referral_code, new_user_id): - db = get_db() - try: - 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 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'])) - db.execute('UPDATE users SET coins = coins + ? WHERE id = ?', (coins_referred, new_user_id)) - db.execute('INSERT INTO referrals (referrer_id, referred_id, reward_coins) VALUES (?, ?, ?)', - (referrer['id'], new_user_id, coins_referrer)) - db.commit() - except Exception as e: - logger.error(f"Error processing affiliate conversion: {e}") - -# ---------- دوال التحليلات ---------- -def track_event(user_id, event_name, event_data=None): - # يمكن تنفيذها لاحقاً - pass -# ---------- دوال الإشعارات والعملات ---------- -def add_notification(user_id, from_user_id, n_type, content, video_id=None, comment_id=None): - if user_id == from_user_id: +def add_notification(user_id, from_id, ntype, content, video_id=None): + if user_id == from_id: return db = get_db() - db.execute(''' - INSERT INTO notifications (user_id, from_user_id, type, content, video_id, comment_id) - 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, - 'content': content, - 'from_user': from_user_id, - 'video_id': video_id, - 'timestamp': datetime.now().isoformat() - }) - -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 notifications (user_id, from_user_id, type, content, video_id) VALUES (?,?,?,?,?)", + (user_id, from_id, ntype, content, video_id)) db.commit() -def deduct_coins(user_id, amount, description): +def add_coins(user_id, amount, desc): db = get_db() - 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.commit() - return True - return False + db.execute("UPDATE users SET coins = coins + ? WHERE id=?", (amount, user_id)) + db.commit() -# ---------- نظام التوصيات ---------- -def get_user_interest_vector(user_id): - db = get_db() - rows = db.execute(''' - SELECT v.vector FROM videos v - JOIN interactions i ON v.id = i.video_id - WHERE i.user_id = ? AND i.liked = 1 AND v.vector IS NOT NULL - 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, limit=10, offset=0): - db = get_db() +# ---------- Cloudinary functions ---------- +def upload_video_to_cloudinary(file_path): 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 = 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, - (SELECT COUNT(*) FROM interactions WHERE video_id = v.id AND liked = 1) as likes, - (SELECT COUNT(*) FROM comments WHERE video_id = v.id) as comments - FROM videos v - 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 + [limit * 3, offset] - rows = db.execute(query, params).fetchall() - 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(''' - SELECT v.*, u.username, u.avatar, u.is_verified - FROM videos v - JOIN users u ON v.user_id = u.id - WHERE v.visibility = 'public' - ORDER BY v.upload_time DESC - LIMIT ? OFFSET ? - ''', (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] + resp = cloudinary.uploader.upload(file_path, resource_type="video", folder="arc_videos") + return resp.get('secure_url'), resp.get('public_id') except Exception as e: - logger.error(f"Error in friends videos: {e}") - return [] + print("Cloudinary error:", e) + return None, None -# ---------- ديكوريتورات ---------- +def video_thumbnail(public_id): + return cloudinary.CloudinaryVideo(public_id).video_thumbnail_url(width=300, height=300) + +def watermark_url(public_id, text="ARC"): + return 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} + ] + ) + +# ---------- Decorators ---------- def login_required(f): @wraps(f) - def decorated_function(*args, **kwargs): + def decorated(*args, **kwargs): if 'user_id' not in session: if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({'error': 'unauthorized', 'redirect': '/login'}), 401 return redirect(url_for('login')) return f(*args, **kwargs) - return decorated_function + return decorated -def admin_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_admin FROM users WHERE id = ?', (session['user_id'],)).fetchone() - if not user or not user['is_admin']: - abort(403) - return f(*args, **kwargs) - return decorated_function - -# ---------- صفحات تسجيل الدخول والتسجيل ---------- +# ---------- Routes: Auth ---------- @app.route('/') def index(): if 'user_id' not in session: return redirect(url_for('login')) - return render_template_string(MAIN_TEMPLATE) + return render_template_string(MAIN_TEMPLATE, session=session) -@app.route('/login', methods=['GET', 'POST']) +@app.route('/login', methods=['GET','POST']) def login(): if request.method == 'POST': - username = request.form.get('username', '') - password = request.form.get('password', '') - remember = request.form.get('remember', False) + username = request.form.get('username','') + password = request.form.get('password','') db = get_db() - user = db.execute('SELECT * FROM users WHERE username = ? OR email = ?', (username, username)).fetchone() + 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'] - if remember: - session.permanent = True - db.execute('UPDATE users SET last_active = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],)) - db.commit() if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return jsonify({'success': True, 'redirect': '/'}) + 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 jsonify({'success': False, 'error':'بيانات غير صحيحة'}), 401 + return render_template_string(LOGIN_PAGE, error='بيانات غير صحيحة') return render_template_string(LOGIN_PAGE) -@app.route('/register', methods=['GET', 'POST']) +@app.route('/register', methods=['GET','POST']) def register(): if request.method == 'POST': - 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','') if not username or not email or not password: return render_template_string(REGISTER_PAGE, error='جميع الحقول مطلوبة') if password != confirm: return render_template_string(REGISTER_PAGE, error='كلمات المرور غير متطابقة') if len(password) < 6: - return render_template_string(REGISTER_PAGE, error='كلمة المرور يجب أن تكون 6 أحرف على الأقل') + return render_template_string(REGISTER_PAGE, error='كلمة المرور قصيرة') db = get_db() - existing = db.execute('SELECT id FROM users WHERE username = ? OR email = ?', (username, email)).fetchone() - if existing: - return render_template_string(REGISTER_PAGE, error='اسم المستخدم أو البريد الإلكتروني موجود بالفعل') + if db.execute("SELECT id FROM users WHERE username=? OR email=?", (username,email)).fetchone(): + 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) + code = secrets.token_hex(4).upper() + db.execute("INSERT INTO users (username, email, password_hash, referral_code) VALUES (?,?,?,?)", + (username, email, hashed, code)) + uid = db.execute("SELECT last_insert_rowid()").fetchone()[0] db.commit() + add_xp(uid, 'register', 50) return redirect(url_for('login')) return render_template_string(REGISTER_PAGE) @@ -832,596 +451,487 @@ def logout(): session.clear() return redirect(url_for('login')) -# ---------- نظام المتابعة (الأصدقاء) ---------- +# ---------- Follow system ---------- @app.route('/follow/', methods=['POST']) @login_required def follow(user_id): if user_id == session['user_id']: - return jsonify({'error': 'لا يمكنك متابعة نفسك'}), 400 + 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.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] + count = db.execute("SELECT COUNT(*) FROM follows WHERE user_id=?", (user_id,)).fetchone()[0] add_xp(session['user_id'], 'follow', 5) - return jsonify({'status': 'ok', 'followers_count': count}) + return jsonify({'status':'ok', 'followers_count':count}) @app.route('/unfollow/', methods=['POST']) @login_required def unfollow(user_id): db = get_db() - db.execute('DELETE FROM follows WHERE user_id = ? AND follower_id = ?', (user_id, session['user_id'])) + 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] - return jsonify({'status': 'ok', 'followers_count': count}) - -@app.route('/api/followers/') -@login_required -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/following/') -@login_required -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) + count = db.execute("SELECT COUNT(*) FROM follows WHERE user_id=?", (user_id,)).fetchone()[0] + return jsonify({'status':'ok', 'followers_count':count}) -# ---------- نظام الفيديوهات مع Cloudinary ---------- -@app.route('/upload', methods=['GET', 'POST']) +# ---------- Video upload ---------- +@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 'لم يتم اختيار فيديو', 400 + return 'لم يتم اختيار فيديو',400 file = request.files['video'] if file.filename == '': - return 'لم يتم اختيار فيديو', 400 - title = request.form.get('title', '') - description = request.form.get('description', '') - music = request.form.get('music', '') + return 'لم يتم اختيار فيديو',400 + title = request.form.get('title','') + desc = request.form.get('description','') + music = request.form.get('music','') 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) - temp_path = os.path.join('/tmp', f"{uuid.uuid4().hex}_{filename}") - file.save(temp_path) - 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) - db = get_db() - cursor = db.execute(''' - INSERT INTO videos ( - 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'], 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() - db.execute('INSERT OR IGNORE INTO hashtags (tag) VALUES (?)', (tag_lower,)) - tag_row = db.execute('SELECT id FROM hashtags WHERE tag = ?', (tag_lower,)).fetchone() - if tag_row: - 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'],)) - db.commit() - os.remove(temp_path) - add_coins(session['user_id'], 10, 'مكافأة نشر فيديو') - add_xp(session['user_id'], 'upload_video', 20) - return redirect(url_for('index')) - return 'نوع الملف غير مدعوم', 400 + visibility = request.form.get('visibility','public') + if not allowed_video(file.filename): + return 'نوع الملف غير مدعوم',400 + # Save temp + temp = os.path.join('/tmp', f"{uuid.uuid4().hex}_{secure_filename(file.filename)}") + file.save(temp) + + # Upload to Cloudinary + url, public_id = upload_video_to_cloudinary(temp) + if not url: + os.remove(temp) + return 'فشل الرفع إلى السحابة',500 + + thumb = video_thumbnail(public_id) + vector = extract_video_features(temp) + nsfw = analyze_nsfw(temp) + tags = generate_auto_tags(temp, title, desc) + + db = get_db() + cur = db.execute(''' + INSERT INTO videos (user_id, cloudinary_url, cloudinary_public_id, thumbnail_url, + title, description, allow_comments, visibility, vector, nsfw_score, ai_tags) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + ''', (session['user_id'], url, public_id, thumb, title, desc, allow_comments, visibility, + vector, nsfw, tags)) + vid = cur.lastrowid + + # Hashtags + for tag in re.findall(r'#(\w+)', desc): + tag_lower = tag.lower() + db.execute("INSERT OR IGNORE INTO hashtags (tag) VALUES (?)", (tag_lower,)) + h = db.execute("SELECT id FROM hashtags WHERE tag=?", (tag_lower,)).fetchone() + if h: + db.execute("INSERT OR IGNORE INTO video_hashtags (video_id, hashtag_id) VALUES (?,?)", (vid, h['id'])) + db.execute("UPDATE hashtags SET usage_count = usage_count + 1 WHERE id=?", (h['id'],)) + db.commit() + os.remove(temp) + + add_coins(session['user_id'], 10, 'رفع فيديو') + add_xp(session['user_id'], 'upload', 20) + + return redirect(url_for('index')) + +# ---------- API: Feeds ---------- @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 + page = int(request.args.get('page',1)) + limit = 10 + offset = (page-1)*limit + db = get_db() + watched = db.execute("SELECT video_id FROM interactions WHERE user_id=? AND watched=1", (session['user_id'],)).fetchall() + watched_ids = [w['video_id'] for w in watched] if watched else [-1] + following = db.execute("SELECT user_id FROM follows WHERE follower_id=?", (session['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 + FROM videos v + 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 + [limit, offset] + rows = db.execute(query, params).fetchall() + videos = [dict(r) for r in rows] + # Prioritize followed users (simple reorder) + videos.sort(key=lambda x: (x['user_id'] in following_ids, x['upload_time']), reverse=True) + result = [] + for v in videos[:limit]: + v['url'] = v['cloudinary_url'] + v['avatar_url'] = f"/avatars/{v['avatar']}" + liked = db.execute("SELECT liked FROM interactions WHERE user_id=? AND video_id=?", (session['user_id'], v['id'])).fetchone() + v['liked_by_user'] = liked['liked'] if liked else False + result.append(v) + next_page = page+1 if len(videos) == limit else None + return jsonify({'videos':result, 'next_page':next_page}) @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 + 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 + 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 ? + ''', (session['user_id'], limit, offset)).fetchall() + result = [] + for v in videos: + v = dict(v) + v['url'] = v['cloudinary_url'] + v['avatar_url'] = f"/avatars/{v['avatar']}" + liked = db.execute("SELECT liked FROM interactions WHERE user_id=? AND video_id=?", (session['user_id'], v['id'])).fetchone() + v['liked_by_user'] = liked['liked'] if liked else False + result.append(v) + next_page = page+1 if len(result) == limit else None + return jsonify({'videos':result, 'next_page':next_page}) @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 + 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 + 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: + v = dict(v) + v['url'] = v['cloudinary_url'] + v['avatar_url'] = f"/avatars/{v['avatar']}" + liked = db.execute("SELECT liked FROM interactions WHERE user_id=? AND video_id=?", (session['user_id'], v['id'])).fetchone() + v['liked_by_user'] = liked['liked'] if liked else False + result.append(v) + next_page = page+1 if len(result) == limit else None + return jsonify({'videos':result, 'next_page':next_page}) @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(''' + 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() + # also search hashtags + tag = db.execute("SELECT id FROM hashtags WHERE tag LIKE ?", ('%'+q+'%',)).fetchone() + if tag: + tag_vids = 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 ?) + JOIN video_hashtags vh ON v.id = vh.video_id + WHERE vh.hashtag_id = ? AND v.visibility='public' 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 = [] + LIMIT 20 + ''', (tag['id'],)).fetchall() + videos = list(videos) + list(tag_vids) + seen = set() + unique = [] 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 - + if v['id'] not in seen: + seen.add(v['id']) + unique.append(v) + videos = unique[:30] + result = [] + for v in videos: + v = dict(v) + v['url'] = v['cloudinary_url'] + v['avatar_url'] = f"/avatars/{v['avatar']}" + result.append(v) + return jsonify({'videos':result}) + +# ---------- API: Interactions ---------- @app.route('/api/like/', methods=['POST']) @login_required -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,)) - liked = False - else: - db.execute(''' - INSERT INTO interactions (user_id, video_id, liked, watched) - VALUES (?, ?, 1, 1) - ON CONFLICT(user_id, video_id) DO UPDATE SET liked = 1 - ''', (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) - 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 +def like(video_id): + user_id = session['user_id'] + db = get_db() + row = db.execute("SELECT liked FROM interactions WHERE user_id=? AND video_id=?", (user_id, video_id)).fetchone() + if row and row['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,)) + liked = False + else: + db.execute(''' + INSERT INTO interactions (user_id, video_id, liked, watched) + VALUES (?,?,1,1) + ON CONFLICT(user_id, video_id) DO UPDATE SET liked=1 + ''', (user_id, video_id)) + db.execute("UPDATE videos SET likes_count = likes_count + 1 WHERE id=?", (video_id,)) + liked = True + # Notification + video = db.execute("SELECT user_id FROM videos WHERE id=?", (video_id,)).fetchone() + if video and video['user_id'] != user_id: + username = db.execute("SELECT username FROM users WHERE id=?", (user_id,)).fetchone()['username'] + add_notification(video['user_id'], user_id, 'like', f'أعجب {username} بفيديو لك', video_id) + db.commit() + likes = db.execute("SELECT likes_count FROM videos WHERE id=?", (video_id,)).fetchone()['likes_count'] + if liked: + add_xp(user_id, 'like', 2) + return jsonify({'status':'ok', 'liked':liked, 'likes_count':likes}) + +@app.route('/api/unlike/', methods=['POST']) +@login_required +def unlike(video_id): + # Same as like with opposite effect, but we can just call like and it toggles. + # However, to avoid duplication, we'll implement a toggle in like. So we can keep this for completeness. + return like(video_id) @app.route('/api/view/', methods=['POST']) @login_required -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) - VALUES (?, ?, 1, ?) - ON CONFLICT(user_id, video_id) DO UPDATE SET - watched = 1, - watch_time = watch_time + ? - ''', (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.commit() - if completed: - add_xp(user_id, 'complete_view', 3) - return jsonify({'status': 'ok'}) - except Exception as e: - logger.error(f"Error in view_video: {e}") - return jsonify({'error': str(e)}), 500 +def view(video_id): + 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() + row = db.execute("SELECT watched FROM interactions WHERE user_id=? AND video_id=?", (user_id, video_id)).fetchone() + if not row or not row['watched']: + db.execute(''' + INSERT INTO interactions (user_id, video_id, watched, watch_time) + VALUES (?,?,1,?) + ON CONFLICT(user_id, video_id) DO UPDATE SET watched=1, watch_time=watch_time+? + ''', (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.commit() + if completed: + add_xp(user_id, 'complete_view', 3) + return jsonify({'status':'ok'}) -@app.route('/api/share/', methods=['POST']) +@app.route('/api/save/', methods=['POST']) @login_required -def share_video(video_id): - try: - user_id = session['user_id'] - db = get_db() +def save(video_id): + user_id = session['user_id'] + db = get_db() + row = db.execute("SELECT saved FROM interactions WHERE user_id=? AND video_id=?", (user_id, video_id)).fetchone() + if row and row['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 = False + else: 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 + 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 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 + db.execute("UPDATE videos SET saves_count = saves_count + 1 WHERE id=?", (video_id,)) + saved = True + db.commit() + saves = db.execute("SELECT saves_count FROM videos WHERE id=?", (video_id,)).fetchone()['saves_count'] + return jsonify({'status':'ok', 'saved':saved, 'saves_count':saves}) -@app.route('/api/save/', methods=['POST']) +@app.route('/api/share/', methods=['POST']) @login_required -def save_video(video_id): - try: - user_id = session['user_id'] - db = get_db() - saved = db.execute('SELECT saved FROM interactions WHERE user_id = ? AND video_id = ?', - (user_id, video_id)).fetchone() - 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 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'] - 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 +def share(video_id): + 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'}) @app.route('/api/saved') @login_required -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 +def saved_videos(): + 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: + v = dict(v) + v['url'] = v['cloudinary_url'] + v['avatar_url'] = f"/avatars/{v['avatar']}" + result.append(v) + return jsonify(result) -# ---------- نظام التعليقات (مختصر) ---------- +# ---------- API: Comments ---------- @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 + db = get_db() + comments = db.execute(''' + SELECT c.*, u.username, u.avatar + 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.timestamp DESC + LIMIT 50 + ''', (video_id,)).fetchall() + result = [] + for c in comments: + cdict = dict(c) + cdict['avatar_url'] = f"/avatars/{c['avatar']}" + cdict['liked_by_user'] = db.execute("SELECT * FROM comment_likes WHERE user_id=? AND comment_id=?", (session['user_id'], c['id'])).fetchone() is not None + # replies + replies = db.execute(''' + SELECT c.*, u.username, u.avatar 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 - 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 + WHERE c.parent_id=? + ORDER BY c.timestamp ASC + ''', (c['id'],)).fetchall() + cdict['replies'] = [] + for r in replies: + rdict = dict(r) + rdict['avatar_url'] = f"/avatars/{r['avatar']}" + rdict['liked_by_user'] = db.execute("SELECT * FROM comment_likes WHERE user_id=? AND comment_id=?", (session['user_id'], r['id'])).fetchone() is not None + cdict['replies'].append(rdict) + result.append(cdict) + return jsonify(result) @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(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 + data = request.get_json() + text = data.get('comment','').strip() + parent = data.get('parent_id') + if not text: + return jsonify({'error':'لا يمكن أن يكون التعليق فارغاً'}),400 + db = get_db() + cur = db.execute(''' + INSERT INTO comments (user_id, video_id, parent_id, comment_text) + VALUES (?,?,?,?) + ''', (session['user_id'], video_id, parent, text)) + cid = cur.lastrowid + db.execute("UPDATE videos SET comments_count = comments_count + 1 WHERE id=?", (video_id,)) + db.commit() + add_xp(session['user_id'], 'comment', 3) + return jsonify({'status':'ok', 'comment_id':cid}) @app.route('/api/comment/like/', methods=['POST']) @login_required 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,)) - liked_status = False - else: - 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'] - 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 + user_id = session['user_id'] + db = get_db() + if db.execute("SELECT * FROM comment_likes WHERE user_id=? AND comment_id=?", (user_id, comment_id)).fetchone(): + 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,)) + liked = False + else: + 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 = True + # Notify comment owner + comment = db.execute("SELECT user_id FROM comments WHERE id=?", (comment_id,)).fetchone() + if comment and comment['user_id'] != user_id: + username = db.execute("SELECT username FROM users WHERE id=?", (user_id,)).fetchone()['username'] + add_notification(comment['user_id'], user_id, 'like_comment', f'أعجب {username} بتعليقك') + db.commit() + likes = db.execute("SELECT likes_count FROM comments WHERE id=?", (comment_id,)).fetchone()['likes_count'] + return jsonify({'status':'ok', 'liked':liked, 'likes_count':likes}) -# ---------- مسارات إضافية: الكاميرا والتسجيل ---------- +# ---------- Camera upload ---------- @app.route('/api/camera/upload', methods=['POST']) @login_required -def upload_recorded_video(): +def camera_upload(): if 'video' not in request.files: - return jsonify({'error': 'لا يوجد فيديو'}), 400 + 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 + temp = os.path.join('/tmp', f"cam_{uuid.uuid4().hex}.mp4") + file.save(temp) + url, public_id = upload_video_to_cloudinary(temp) + os.remove(temp) + if not 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 + cur = db.execute(''' + INSERT INTO videos (user_id, cloudinary_url, cloudinary_public_id, title, visibility) + VALUES (?,?,?,?,?) + ''', (session['user_id'], url, public_id, 'تسجيل كاميرا', 'public')) + vid = cur.lastrowid db.commit() - add_xp(session['user_id'], 'upload_video', 20) - return jsonify({'status': 'ok', 'video_id': video_id, 'url': cloudinary_url}) + add_xp(session['user_id'], 'upload', 20) + return jsonify({'status':'ok', 'video_id':vid, 'url':url}) -# ---------- مسار العلامة المائية ---------- +# ---------- Watermark ---------- @app.route('/api/watermark/', methods=['POST']) @login_required -def watermark_video(video_id): - try: - db = get_db() - 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"Watermark error: {e}") - return jsonify({'error': str(e)}), 500 +def watermark(video_id): + db = get_db() + vid = db.execute("SELECT cloudinary_public_id FROM videos WHERE id=?", (video_id,)).fetchone() + if not vid: + return jsonify({'error':'فيديو غير موجود'}),404 + wm_url = watermark_url(vid['cloudinary_public_id'], text="ARC") + return jsonify({'watermarked_url': wm_url}) -# ---------- مسارات API للبروفايل ---------- +# ---------- Profile API ---------- @app.route('/api/profile/') @login_required -def api_profile(user_id): +def profile_api(user_id): db = get_db() - user = db.execute(''' - SELECT u.*, s.* - FROM users u - LEFT JOIN user_settings s ON u.id = s.user_id - WHERE u.id = ? - ''', (user_id,)).fetchone() + user = db.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone() if not user: - return jsonify({'error': 'المستخدم غير موجود'}), 404 + 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' + SELECT id, title, cloudinary_url, thumbnail_url, views, likes_count, comments_count + 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] + 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] 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 + 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({ 'user': dict(user), 'videos': [dict(v) for v in videos], - 'followers_count': followers_count, - 'following_count': following_count, + 'followers_count': followers, + 'following_count': following, 'is_following': is_following, 'total_views': total_views }) -# ---------- مسارات الطبقة الزجاجية ---------- +# ---------- Glass routes (SPA partials) ---------- @app.route('/glass/profile/') @login_required def glass_profile(user_id): - return render_template_string(GLASS_PROFILE_TEMPLATE, user_id=user_id) + return render_template_string(GLASS_PROFILE_TEMPLATE, user_id=user_id, session=session) @app.route('/glass/saved') @login_required @@ -1431,155 +941,55 @@ def glass_saved(): @app.route('/glass/friends') @login_required def glass_friends(): - return render_template_string(GLASS_FRIENDS_TEMPLATE) + return render_template_string(GLASS_FRIENDS_TEMPLATE, session=session) @app.route('/glass/search') @login_required def glass_search(): return render_template_string(GLASS_SEARCH_TEMPLATE) -# ---------- دوال الملفات (للصور الشخصية) ---------- +# ---------- Serve avatars ---------- @app.route('/avatars/') def serve_avatar(filename): - return send_from_directory(app.config['AVATAR_FOLDER'], filename) + return send_from_directory(AVATAR_FOLDER, filename) -# ---------- قوالب HTML (معاد تصميمها) ---------- +# ---------- HTML Templates (embedded) ---------- LOGIN_PAGE = ''' - - - - تسجيل الدخول - ARC - + +تسجيل الدخول - ARC + -
-

ARC

-
-
- - -
-
- - -
-
- - -
- -
- {% if error %}
{{ error }}
{% endif %} - -
+
+

ARC

+
+
+
+
+ +
+{% if error %}
{{ error }}
{% endif %} + +
''' @@ -1587,132 +997,40 @@ LOGIN_PAGE = ''' REGISTER_PAGE = ''' - - - - تسجيل جديد - ARC - + +تسجيل جديد - ARC + -
-

ARC

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -
- {% if error %}
{{ error }}
{% endif %} - -
+
+

ARC

+
+
+
+
+
+
+ +
+{% if error %}
{{ error }}
{% endif %} + +
''' @@ -1720,292 +1038,86 @@ REGISTER_PAGE = ''' UPLOAD_TEMPLATE = ''' - - - - رفع فيديو - ARC - + +رفع فيديو - ARC + -
-

رفع فيديو جديد

- -
- -
- - - - -
- -
- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- السماح بالتعليقات -
-
- السماح بالثنائي -
-
- السماح بالمونتاج -
-
- -
-
📁 اختر فيديو من الجهاز
- -
- - -
- العودة للرئيسية -
- - +
+

رفع فيديو جديد

+
+ +
+ + + + +
+ +
+
+
+
+
+
+
السماح بالتعليقات
+
📁 اختر فيديو من الجهاز
+ +
+العودة للرئيسية +
+ ''' @@ -2014,554 +1126,116 @@ MAIN_TEMPLATE = ''' - - - ARC - فيديوهات قصيرة - + + +ARC - فيديوهات قصيرة + -
- - - -
-
- -
-
-
- -
❤️
- - +
+ +
+
❤️
+ ''' GLASS_PROFILE_TEMPLATE = ''' -
-

الملف الشخصي

-
جاري التحميل...
-
+

الملف الشخصي

جاري التحميل...
''' GLASS_SAVED_TEMPLATE = ''' -
-

الفيديوهات المحفوظة

-
-
+

الفيديوهات المحفوظة

''' GLASS_FRIENDS_TEMPLATE = ''' -
-

الأصدقاء

-
-
+

الأصدقاء

''' GLASS_SEARCH_TEMPLATE = ''' -
-

بحث

- -
-
+

بحث

''' -# ---------- تشغيل التطبيق ---------- +# ---------- Run ---------- if __name__ == '__main__': with app.app_context(): - try: - init_db() - print("✅ تم تهيئة قاعدة البيانات بنجاح") - except Exception as e: - print(f"❌ خطأ أثناء التهيئة: {e}") - - print("=" * 60) - print("🚀 ARC Video - النسخة الاحترافية على Hugging Face") - print("=" * 60) - - app.run(host='0.0.0.0', port=7860) \ No newline at end of file + init_db() + app.run(host='0.0.0.0', port=7860, debug=True) \ No newline at end of file