| 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 |
| from typing import Dict, List, Optional, Tuple, Any |
| from dataclasses import dataclass |
| from enum import Enum |
| import hmac |
|
|
| |
| import cloudinary |
| import cloudinary.uploader |
| import cloudinary.api |
|
|
| |
| os.makedirs('logs', exist_ok=True) |
| os.makedirs('videos', exist_ok=True) |
| os.makedirs('avatars', exist_ok=True) |
| os.makedirs('thumbnails', exist_ok=True) |
| os.makedirs('encrypted', exist_ok=True) |
| os.makedirs('watermarked', exist_ok=True) |
| os.makedirs('affiliate_logs', exist_ok=True) |
| os.makedirs('cache', exist_ok=True) |
|
|
| |
| logging.basicConfig( |
| filename='logs/app.log', |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
| try: |
| from PIL import Image, ImageDraw, ImageFont, ImageFilter |
| HAS_PIL = True |
| except ImportError: |
| HAS_PIL = False |
| logger.warning("PIL غير مثبت، ميزات الصور محدودة.") |
|
|
| try: |
| import cv2 |
| import face_recognition |
| HAS_CV2 = True |
| HAS_FACE_RECOGNITION = True |
| except ImportError: |
| HAS_CV2 = False |
| HAS_FACE_RECOGNITION = False |
| logger.warning("OpenCV أو face_recognition غير مثبتين.") |
|
|
| try: |
| from Crypto.Cipher import AES |
| from Crypto.Util.Padding import pad, unpad |
| from Crypto.Random import get_random_bytes |
| HAS_CRYPTO = True |
| except ImportError: |
| HAS_CRYPTO = False |
| logger.warning("PyCryptodome غير مثبت، التشفير المتقدم لن يعمل.") |
|
|
| try: |
| import redis |
| HAS_REDIS = True |
| REDIS_CLIENT = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True) |
| except ImportError: |
| HAS_REDIS = False |
| REDIS_CLIENT = None |
| logger.warning("Redis غير مثبت، سيتم استخدام الذاكرة المؤقتة المحلية.") |
|
|
| try: |
| import pyotp |
| import qrcode |
| from io import BytesIO |
| HAS_2FA = True |
| except ImportError: |
| HAS_2FA = False |
| logger.warning("pyotp أو qrcode غير مثبتين، المصادقة الثنائية غير متاحة.") |
|
|
| try: |
| from flask_limiter import Limiter |
| from flask_limiter.util import get_remote_address |
| HAS_LIMITER = True |
| except ImportError: |
| HAS_LIMITER = False |
| logger.warning("Flask-Limiter غير مثبت، حماية Rate Limiting غير متاحة.") |
|
|
| from flask import ( |
| Flask, request, session, g, jsonify, render_template_string, |
| redirect, url_for, abort, send_from_directory, Response, |
| make_response, flash |
| ) |
| from werkzeug.utils import secure_filename |
| from werkzeug.security import generate_password_hash, check_password_hash |
|
|
| |
| cloudinary.config( |
| cloud_name="dpylnwrw0", |
| api_key="631276857136451", |
| api_secret="xpehguQcV_7nj0iBXsMNM5PssHE" |
| ) |
|
|
| app = Flask(__name__) |
| app.secret_key = secrets.token_hex(64) |
| app.config['SESSION_TYPE'] = 'filesystem' |
| app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) |
| app.config['JSON_AS_ASCII'] = False |
| app.config['SESSION_COOKIE_SECURE'] = True |
| app.config['SESSION_COOKIE_HTTPONLY'] = True |
| app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' |
| app.config['REMEMBER_COOKIE_SECURE'] = True |
| app.config['REMEMBER_COOKIE_HTTPONLY'] = True |
|
|
| |
| if HAS_LIMITER: |
| limiter = Limiter( |
| app=app, |
| key_func=get_remote_address, |
| default_limits=["200 per day", "50 per hour"], |
| storage_uri="memory://" |
| ) |
| else: |
| |
| class limiter: |
| @staticmethod |
| def limit(limit_string): |
| def decorator(f): |
| return f |
| return decorator |
|
|
| |
| UPLOAD_FOLDER = os.path.join(os.getcwd(), 'videos') |
| AVATAR_FOLDER = os.path.join(os.getcwd(), 'avatars') |
| THUMBNAIL_FOLDER = os.path.join(os.getcwd(), 'thumbnails') |
| ENCRYPTED_FOLDER = os.path.join(os.getcwd(), 'encrypted') |
| WATERMARK_FOLDER = os.path.join(os.getcwd(), 'watermarked') |
| CACHE_FOLDER = os.path.join(os.getcwd(), 'cache') |
|
|
| ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm', '3gp', 'm4v'} |
| ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'} |
| ALLOWED_PLUGIN_EXTENSIONS = {'py'} |
|
|
| app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER |
| app.config['AVATAR_FOLDER'] = AVATAR_FOLDER |
| app.config['THUMBNAIL_FOLDER'] = THUMBNAIL_FOLDER |
| app.config['ENCRYPTED_FOLDER'] = ENCRYPTED_FOLDER |
| app.config['WATERMARK_FOLDER'] = WATERMARK_FOLDER |
| app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 |
|
|
| |
| MASTER_ENCRYPTION_KEY = hashlib.sha256(app.secret_key.encode()).digest() |
|
|
| |
| DATABASE = os.path.join(os.getcwd(), 'arc_video_pro.db') |
| VECTOR_DIM = 64 |
|
|
| |
| notification_queues = {} |
| active_streams = {} |
| cache_storage = {} |
| trending_cache = {'videos': [], 'hashtags': [], 'updated_at': None} |
| recommendation_cache = {} |
|
|
| |
| class UserRole(Enum): |
| USER = 'user' |
| CREATOR = 'creator' |
| VIP = 'vip' |
| VIP_GOLD = 'vip_gold' |
| MODERATOR = 'moderator' |
| ADMIN = 'admin' |
| DEVELOPER = 'developer' |
|
|
| class MembershipTier(Enum): |
| FREE = 'free' |
| PREMIUM = 'premium' |
| VIP = 'vip' |
| VIP_GOLD = 'vip_gold' |
|
|
| @dataclass |
| class MembershipBenefits: |
| max_video_size: int = 500 * 1024 * 1024 |
| max_video_duration: int = 180 |
| can_upload_4k: bool = False |
| has_gold_badge: bool = False |
| no_ads: bool = False |
| can_live_stream: bool = False |
| priority_support: bool = False |
| monthly_coins: int = 0 |
|
|
| MEMBERSHIP_BENEFITS = { |
| MembershipTier.FREE: MembershipBenefits(), |
| MembershipTier.PREMIUM: MembershipBenefits( |
| max_video_size=1024 * 1024 * 1024, |
| max_video_duration=600, |
| can_upload_4k=True, |
| no_ads=True, |
| monthly_coins=100 |
| ), |
| MembershipTier.VIP: MembershipBenefits( |
| max_video_size=2 * 1024 * 1024 * 1024, |
| max_video_duration=1800, |
| can_upload_4k=True, |
| has_gold_badge=True, |
| no_ads=True, |
| can_live_stream=True, |
| priority_support=True, |
| monthly_coins=500 |
| ), |
| MembershipTier.VIP_GOLD: MembershipBenefits( |
| max_video_size=5 * 1024 * 1024 * 1024, |
| max_video_duration=3600, |
| can_upload_4k=True, |
| has_gold_badge=True, |
| no_ads=True, |
| can_live_stream=True, |
| priority_support=True, |
| monthly_coins=2000 |
| ) |
| } |
|
|
| |
| def get_db(): |
| db = getattr(g, '_database', None) |
| if db is None: |
| db = g._database = sqlite3.connect(DATABASE) |
| db.row_factory = sqlite3.Row |
| db.execute("PRAGMA foreign_keys = ON") |
| db.execute("PRAGMA journal_mode = WAL") |
| db.execute("PRAGMA synchronous = NORMAL") |
| db.execute("PRAGMA cache_size = 10000") |
| db.execute("PRAGMA temp_store = MEMORY") |
| return db |
|
|
| @app.teardown_appcontext |
| def close_connection(exception): |
| db = getattr(g, '_database', None) |
| if db is not None: |
| db.close() |
|
|
| def query_db(query, args=(), one=False): |
| """دالة مساعدة للاستعلامات مع التخزين المؤقت""" |
| cur = get_db().execute(query, args) |
| rv = cur.fetchall() |
| cur.close() |
| return (rv[0] if rv else None) if one else rv |
|
|
| def init_db(): |
| """إنشاء قاعدة البيانات المتقدمة""" |
| with app.app_context(): |
| db = get_db() |
| cursor = db.cursor() |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS users ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| username TEXT UNIQUE NOT NULL, |
| email TEXT UNIQUE NOT NULL, |
| password_hash TEXT NOT NULL, |
| 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, |
| role TEXT DEFAULT 'user', |
| membership_tier TEXT DEFAULT 'free', |
| membership_expires TIMESTAMP, |
| is_live BOOLEAN DEFAULT 0, |
| coins INTEGER DEFAULT 100, |
| diamonds INTEGER DEFAULT 0, |
| xp INTEGER DEFAULT 0, |
| level INTEGER DEFAULT 1, |
| referral_code TEXT UNIQUE, |
| referred_by INTEGER, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| |
| -- إحصائيات متقدمة |
| total_views INTEGER DEFAULT 0, |
| total_likes_received INTEGER DEFAULT 0, |
| total_comments_received INTEGER DEFAULT 0, |
| total_followers INTEGER DEFAULT 0, |
| total_following INTEGER DEFAULT 0, |
| |
| -- أمان متقدم |
| two_factor_secret TEXT, |
| two_factor_enabled BOOLEAN DEFAULT 0, |
| backup_codes TEXT, |
| login_attempts INTEGER DEFAULT 0, |
| locked_until TIMESTAMP, |
| api_key TEXT UNIQUE, |
| api_quota INTEGER DEFAULT 1000, |
| |
| -- إحالات |
| 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) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS sessions ( |
| id TEXT PRIMARY KEY, |
| user_id INTEGER NOT NULL, |
| ip_address TEXT, |
| user_agent TEXT, |
| 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 user_settings ( |
| user_id INTEGER PRIMARY KEY, |
| privacy_profile TEXT DEFAULT 'public', |
| privacy_videos TEXT DEFAULT 'public', |
| privacy_likes TEXT DEFAULT 'public', |
| notifications_likes BOOLEAN DEFAULT 1, |
| notifications_comments BOOLEAN DEFAULT 1, |
| notifications_follows BOOLEAN DEFAULT 1, |
| notifications_messages BOOLEAN DEFAULT 1, |
| notifications_live BOOLEAN DEFAULT 1, |
| notifications_gifts BOOLEAN DEFAULT 1, |
| dark_mode BOOLEAN DEFAULT 1, |
| language TEXT DEFAULT 'ar', |
| content_language TEXT DEFAULT 'ar', |
| autoplay BOOLEAN DEFAULT 1, |
| save_data BOOLEAN DEFAULT 0, |
| allow_download BOOLEAN DEFAULT 1, |
| allow_duet BOOLEAN DEFAULT 1, |
| allow_stitch BOOLEAN DEFAULT 1, |
| FOREIGN KEY(user_id) REFERENCES users(id) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS follows ( |
| user_id INTEGER, |
| follower_id INTEGER, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(follower_id) REFERENCES users(id) ON DELETE CASCADE, |
| PRIMARY KEY (user_id, follower_id) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS videos ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER NOT NULL, |
| |
| -- الملفات |
| filename TEXT NOT NULL, |
| filepath TEXT NOT NULL, |
| filesize INTEGER DEFAULT 0, |
| duration INTEGER DEFAULT 0, |
| |
| -- Cloudinary |
| cloudinary_url TEXT, |
| cloudinary_public_id TEXT, |
| thumbnail TEXT, |
| |
| -- محتوى |
| title TEXT, |
| description TEXT, |
| music TEXT, |
| music_id INTEGER, |
| |
| -- إعدادات |
| 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, |
| likes_count INTEGER DEFAULT 0, |
| shares_count INTEGER DEFAULT 0, |
| saves_count INTEGER DEFAULT 0, |
| comments_count INTEGER DEFAULT 0, |
| |
| -- تحليلات متقدمة |
| avg_watch_time REAL DEFAULT 0, |
| completion_rate REAL DEFAULT 0, |
| |
| -- بيانات AI |
| vector BLOB, |
| nsfw_score REAL DEFAULT 0.0, |
| ai_tags TEXT, |
| processed_for_ai BOOLEAN DEFAULT 0, |
| |
| -- أمان |
| encrypted_path TEXT, |
| encrypted_key BLOB, |
| watermarked_path TEXT, |
| |
| -- تحديات |
| challenge_id INTEGER, |
| |
| -- تقارير |
| is_reported BOOLEAN DEFAULT 0, |
| report_count INTEGER DEFAULT 0, |
| |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(challenge_id) REFERENCES challenges(id) |
| ) |
| ''') |
| |
| |
| 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, |
| watch_time INTEGER DEFAULT 0, |
| watch_percentage REAL DEFAULT 0, |
| completed BOOLEAN DEFAULT 0, |
| voted_challenge BOOLEAN DEFAULT 0, |
| timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(video_id) REFERENCES videos(id) ON DELETE CASCADE, |
| UNIQUE(user_id, video_id) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS comments ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER NOT NULL, |
| video_id INTEGER NOT NULL, |
| parent_id INTEGER, |
| comment_text TEXT NOT NULL, |
| likes_count INTEGER DEFAULT 0, |
| is_pinned BOOLEAN DEFAULT 0, |
| is_edited BOOLEAN DEFAULT 0, |
| edit_history TEXT, |
| is_reported BOOLEAN DEFAULT 0, |
| report_count INTEGER DEFAULT 0, |
| is_moderated BOOLEAN DEFAULT 0, |
| moderation_action TEXT, |
| timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(video_id) REFERENCES videos(id) ON DELETE CASCADE, |
| FOREIGN KEY(parent_id) REFERENCES comments(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| 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, |
| watch_percentage REAL DEFAULT 0, |
| completed BOOLEAN DEFAULT 0, |
| ip_address TEXT, |
| user_agent TEXT, |
| viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(video_id) REFERENCES videos(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS hashtags ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| tag TEXT UNIQUE NOT NULL, |
| usage_count INTEGER DEFAULT 1, |
| total_views INTEGER DEFAULT 0, |
| total_likes INTEGER DEFAULT 0, |
| trending_score REAL DEFAULT 0, |
| last_trending_update TIMESTAMP, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS video_hashtags ( |
| video_id INTEGER NOT NULL, |
| hashtag_id INTEGER NOT NULL, |
| FOREIGN KEY(video_id) REFERENCES videos(id) ON DELETE CASCADE, |
| FOREIGN KEY(hashtag_id) REFERENCES hashtags(id) ON DELETE CASCADE, |
| PRIMARY KEY (video_id, hashtag_id) |
| ) |
| ''') |
| |
| |
| 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, |
| content TEXT, |
| video_id INTEGER, |
| comment_id INTEGER, |
| gift_id INTEGER, |
| is_read BOOLEAN DEFAULT 0, |
| is_seen BOOLEAN DEFAULT 0, |
| priority INTEGER DEFAULT 0, |
| action_url TEXT, |
| image_url TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| expires_at TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(from_user_id) REFERENCES users(id) ON DELETE SET NULL, |
| FOREIGN KEY(video_id) REFERENCES videos(id) ON DELETE CASCADE, |
| FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| 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, |
| is_delivered BOOLEAN DEFAULT 0, |
| is_deleted BOOLEAN DEFAULT 0, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(sender_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(receiver_id) REFERENCES users(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS conversations ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user1_id INTEGER NOT NULL, |
| user2_id INTEGER NOT NULL, |
| last_message TEXT, |
| last_message_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| unread_count_user1 INTEGER DEFAULT 0, |
| unread_count_user2 INTEGER DEFAULT 0, |
| FOREIGN KEY(user1_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(user2_id) REFERENCES users(id) ON DELETE CASCADE, |
| UNIQUE(user1_id, user2_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, |
| prize_diamonds INTEGER DEFAULT 0, |
| start_date DATE, |
| end_date DATE, |
| is_active BOOLEAN DEFAULT 1, |
| winner_id INTEGER, |
| total_participants INTEGER DEFAULT 0, |
| total_votes INTEGER DEFAULT 0, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(winner_id) REFERENCES users(id) ON DELETE SET NULL |
| ) |
| ''') |
| |
| |
| 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, |
| rank INTEGER, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(video_id) REFERENCES videos(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS challenge_votes ( |
| user_id INTEGER NOT NULL, |
| participant_id INTEGER NOT NULL, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(participant_id) REFERENCES challenge_participants(id) ON DELETE CASCADE, |
| PRIMARY KEY (user_id, participant_id) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS live_streams ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER NOT NULL, |
| stream_key TEXT UNIQUE NOT NULL, |
| title TEXT, |
| description TEXT, |
| thumbnail TEXT, |
| viewers INTEGER DEFAULT 0, |
| peak_viewers INTEGER DEFAULT 0, |
| total_watch_time INTEGER DEFAULT 0, |
| likes INTEGER DEFAULT 0, |
| gifts_value INTEGER DEFAULT 0, |
| started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| ended_at TIMESTAMP, |
| is_active BOOLEAN DEFAULT 1, |
| recording_url TEXT, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS live_gifts ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| stream_id INTEGER NOT NULL, |
| user_id INTEGER NOT NULL, |
| gift_type TEXT NOT NULL, |
| gift_value INTEGER NOT NULL, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(stream_id) REFERENCES live_streams(id) ON DELETE CASCADE, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS virtual_gifts ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| name TEXT NOT NULL, |
| price INTEGER NOT NULL, |
| animation_url TEXT, |
| image_url TEXT, |
| is_active BOOLEAN DEFAULT 1 |
| ) |
| ''') |
| |
| |
| 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) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| 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, |
| reward_xp INTEGER DEFAULT 0, |
| streak INTEGER DEFAULT 1, |
| claimed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| UNIQUE(user_id, reward_date) |
| ) |
| ''') |
| |
| |
| 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, |
| currency TEXT DEFAULT 'coins', |
| balance_after INTEGER, |
| description TEXT, |
| reference_id TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| 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, |
| reward_xp INTEGER DEFAULT 20, |
| status TEXT DEFAULT 'completed', |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(referrer_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(referred_id) REFERENCES users(id) ON DELETE CASCADE, |
| UNIQUE(referred_id) |
| ) |
| ''') |
| |
| |
| 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) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| 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, |
| details TEXT, |
| status TEXT DEFAULT 'pending', |
| action_taken TEXT, |
| handled_by INTEGER, |
| handled_at TIMESTAMP, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(reporter_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(reported_user_id) REFERENCES users(id) ON DELETE CASCADE, |
| FOREIGN KEY(reported_video_id) REFERENCES videos(id) ON DELETE CASCADE, |
| FOREIGN KEY(reported_comment_id) REFERENCES comments(id) ON DELETE CASCADE, |
| FOREIGN KEY(handled_by) REFERENCES users(id) ON DELETE SET NULL |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS developer_plugins ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER NOT NULL, |
| name TEXT NOT NULL, |
| filename TEXT NOT NULL, |
| code TEXT NOT NULL, |
| is_active BOOLEAN DEFAULT 1, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| updated_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) ON DELETE CASCADE |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS analytics_events ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| session_id TEXT, |
| event_name TEXT NOT NULL, |
| event_data TEXT, |
| page_url TEXT, |
| ip_address TEXT, |
| user_agent TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS music_tracks ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| title TEXT NOT NULL, |
| artist TEXT, |
| album TEXT, |
| duration INTEGER, |
| url TEXT, |
| cloudinary_public_id TEXT, |
| cover_image TEXT, |
| genre TEXT, |
| usage_count INTEGER DEFAULT 0, |
| is_active BOOLEAN DEFAULT 1, |
| uploaded_by INTEGER, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS search_queries ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| query TEXT NOT NULL, |
| results_count INTEGER, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL |
| ) |
| ''') |
| |
| |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_videos_user_id ON videos(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_videos_upload_time ON videos(upload_time DESC)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_videos_views ON videos(views DESC)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_videos_nsfw ON videos(nsfw_score)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_comments_video_id ON comments(video_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_comments_user_id ON comments(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_interactions_user_id ON interactions(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_interactions_video_id ON interactions(video_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id, is_read, created_at DESC)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_follows_user_id ON follows(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_follows_follower_id ON follows(follower_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_hashtags_tag ON hashtags(tag)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_hashtags_trending ON hashtags(trending_score DESC)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_video_views_video_id ON video_views(video_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_transactions_user_id ON transactions(user_id, created_at DESC)') |
| |
| db.commit() |
| |
| |
| create_default_data() |
| logger.info("✅ تم تهيئة قاعدة البيانات بنجاح") |
|
|
| def create_default_data(): |
| """إنشاء البيانات الافتراضية""" |
| db = get_db() |
| |
| |
| admin = db.execute("SELECT id FROM users WHERE username = 'admin'").fetchone() |
| if not admin: |
| hashed = generate_password_hash('Admin@123456') |
| referral = generate_referral_code() |
| cursor = db.execute(''' |
| INSERT INTO users ( |
| username, email, password_hash, role, referral_code, |
| is_developer, is_verified, coins, diamonds, xp, level |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| ''', ('admin', 'admin@arc.com', hashed, 'admin', referral, 1, 1, 10000, 1000, 10000, 50)) |
| |
| admin_id = cursor.lastrowid |
| db.execute('INSERT INTO user_settings (user_id) VALUES (?)', (admin_id,)) |
| db.commit() |
| |
| |
| vip = db.execute("SELECT id FROM users WHERE username = 'vip'").fetchone() |
| if not vip: |
| hashed = generate_password_hash('Vip@123456') |
| referral = generate_referral_code() |
| cursor = db.execute(''' |
| INSERT INTO users ( |
| username, email, password_hash, role, membership_tier, |
| referral_code, is_verified, coins, diamonds, xp, level |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| ''', ('vip', 'vip@arc.com', hashed, 'vip', 'vip_gold', referral, 1, 5000, 500, 5000, 30)) |
| |
| vip_id = cursor.lastrowid |
| db.execute('INSERT INTO user_settings (user_id) VALUES (?)', (vip_id,)) |
| db.commit() |
| |
| |
| creator = db.execute("SELECT id FROM users WHERE username = 'creator'").fetchone() |
| if not creator: |
| hashed = generate_password_hash('Creator@123456') |
| referral = generate_referral_code() |
| cursor = db.execute(''' |
| INSERT INTO users ( |
| username, email, password_hash, role, referral_code, |
| is_verified, coins, diamonds, xp, level |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| ''', ('creator', 'creator@arc.com', hashed, 'creator', referral, 1, 2000, 100, 2000, 15)) |
| |
| creator_id = cursor.lastrowid |
| db.execute('INSERT INTO user_settings (user_id) VALUES (?)', (creator_id,)) |
| db.commit() |
| |
| |
| gifts = db.execute("SELECT id FROM virtual_gifts").fetchall() |
| if not gifts: |
| default_gifts = [ |
| ('وردة', 10, '🌹', '/static/gifts/rose.gif'), |
| ('تاج', 50, '👑', '/static/gifts/crown.gif'), |
| ('سيارة', 100, '🚗', '/static/gifts/car.gif'), |
| ('يخت', 500, '🛥️', '/static/gifts/yacht.gif'), |
| ('قصر', 1000, '🏰', '/static/gifts/castle.gif'), |
| ('صاروخ', 2000, '🚀', '/static/gifts/rocket.gif'), |
| ('مجرة', 5000, '🌌', '/static/gifts/galaxy.gif'), |
| ] |
| for name, price, emoji, url in default_gifts: |
| db.execute(''' |
| INSERT INTO virtual_gifts (name, price, animation_url, image_url) |
| VALUES (?, ?, ?, ?) |
| ''', (f'{emoji} {name}', price, url, url)) |
| db.commit() |
| |
| |
| challenges = db.execute("SELECT id FROM challenges").fetchall() |
| if not challenges: |
| today = datetime.now().strftime('%Y-%m-%d') |
| next_week = (datetime.now() + timedelta(days=7)).strftime('%Y-%m-%d') |
| next_month = (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d') |
| |
| challenges_data = [ |
| ('تحدي الرقص', 'ارقص أفضل رقصة واربح 1000 عملة', 1000, 50, today, next_week), |
| ('تحدي الطبخ', 'اطبخ ألذ طبق في 60 ثانية', 1500, 100, today, next_week), |
| ('تحدي الضحك', 'أضحكنا من قلبك', 500, 20, today, next_week), |
| ('تحدي التقليد', 'قلد أي شخصية مشهورة', 2000, 150, today, next_month), |
| ('تحدي المواهب', 'أظهر موهبتك الفريدة', 3000, 200, today, next_month), |
| ('تحدي الجمال', 'شاركنا روتين جمالك', 800, 30, today, next_week), |
| ('تحدي اللياقة', 'تمرين سريع في 5 دقائق', 1200, 60, today, next_week), |
| ] |
| for c in challenges_data: |
| db.execute(''' |
| INSERT INTO challenges (title, description, prize_coins, prize_diamonds, start_date, end_date) |
| VALUES (?, ?, ?, ?, ?, ?) |
| ''', c) |
| db.commit() |
| |
| |
| music = db.execute("SELECT id FROM music_tracks").fetchall() |
| if not music: |
| music_data = [ |
| ('صوت الطبيعة', 'ARC', 'أصوات', 180, None), |
| ('إيقاعات حماسية', 'ARC', 'إلكتروني', 210, None), |
| ('لحن هادئ', 'ARC', 'موسيقى تصويرية', 240, None), |
| ('تحدي الرقص', 'ARC', 'بوب', 195, None), |
| ('مقدمة فيديو', 'ARC', 'إعلاني', 90, None), |
| ] |
| for title, artist, genre, duration, url in music_data: |
| db.execute(''' |
| INSERT INTO music_tracks (title, artist, genre, duration, url, is_active) |
| VALUES (?, ?, ?, ?, ?, 1) |
| ''', (title, artist, genre, duration, url)) |
| db.commit() |
|
|
| |
| def generate_referral_code(): |
| """توليد كود إحالة فريد""" |
| return secrets.token_hex(6).upper() |
|
|
| def random_vector(dim=VECTOR_DIM): |
| """توليد متجه عشوائي للفيديوهات الجديدة""" |
| vec = np.random.randn(dim).astype(np.float32) |
| return vec.tobytes() |
|
|
| def bytes_to_vector(b): |
| """تحويل البيانات الثنائية إلى متجه numpy""" |
| if b is None: |
| return np.zeros(VECTOR_DIM, dtype=np.float32) |
| return np.frombuffer(b, dtype=np.float32) |
|
|
| def allowed_file(filename, allowed_extensions): |
| """التحقق من امتداد الملف""" |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions |
|
|
| def allowed_video(filename): |
| return allowed_file(filename, 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) |
|
|
| def get_client_ip(): |
| """الحصول على عنوان IP العميل""" |
| if request.headers.get('X-Forwarded-For'): |
| return request.headers.get('X-Forwarded-For').split(',')[0].strip() |
| return request.remote_addr |
|
|
| def get_user_membership_tier(user_id): |
| """الحصول على مستوى عضوية المستخدم""" |
| user = query_db('SELECT membership_tier, membership_expires FROM users WHERE id = ?', |
| (user_id,), one=True) |
| if not user: |
| return MembershipTier.FREE |
| |
| |
| if user['membership_expires']: |
| expires = datetime.strptime(user['membership_expires'], '%Y-%m-%d %H:%M:%S') |
| if expires < datetime.now(): |
| return MembershipTier.FREE |
| |
| try: |
| return MembershipTier(user['membership_tier']) |
| except: |
| return MembershipTier.FREE |
|
|
| def get_membership_benefits(user_id): |
| """الحصول على مزايا العضوية للمستخدم""" |
| tier = get_user_membership_tier(user_id) |
| return MEMBERSHIP_BENEFITS[tier] |
|
|
| def cache_get(key): |
| """الحصول من التخزين المؤقت""" |
| if HAS_REDIS and REDIS_CLIENT: |
| return REDIS_CLIENT.get(key) |
| return cache_storage.get(key) |
|
|
| def cache_set(key, value, timeout=300): |
| """تخزين في الذاكرة المؤقتة""" |
| if HAS_REDIS and REDIS_CLIENT: |
| REDIS_CLIENT.setex(key, timeout, value) |
| else: |
| cache_storage[key] = { |
| 'value': value, |
| 'expires': time.time() + timeout |
| } |
|
|
| def cache_delete(key): |
| """حذف من التخزين المؤقت""" |
| if HAS_REDIS and REDIS_CLIENT: |
| REDIS_CLIENT.delete(key) |
| else: |
| cache_storage.pop(key, None) |
|
|
| |
| def analyze_video_content(video_path): |
| """تحليل متقدم لمحتوى الفيديو""" |
| results = { |
| 'nsfw_score': 0.0, |
| 'faces_count': 0, |
| 'has_text': False, |
| 'brightness': 0.5, |
| 'motion_score': 0.0, |
| 'scene_type': 'unknown', |
| 'tags': [] |
| } |
| |
| if not HAS_CV2: |
| return results |
| |
| try: |
| cap = cv2.VideoCapture(video_path) |
| if not cap.isOpened(): |
| return results |
| |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| if total_frames == 0: |
| cap.release() |
| return results |
| |
| |
| sample_frames = min(20, total_frames) |
| frame_indices = [int(i * total_frames / sample_frames) for i in range(sample_frames)] |
| |
| skin_tone_frames = 0 |
| face_frames = 0 |
| brightness_sum = 0 |
| motion_sum = 0 |
| prev_frame = None |
| |
| for idx in frame_indices: |
| cap.set(cv2.CAP_PROP_POS_FRAMES, idx) |
| ret, frame = cap.read() |
| if not ret: |
| continue |
| |
| |
| 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) |
| skin_mask = cv2.inRange(hsv, lower_skin, upper_skin) |
| skin_percentage = (cv2.countNonZero(skin_mask) / (frame.shape[0] * frame.shape[1])) * 100 |
| if skin_percentage > 30: |
| skin_tone_frames += 1 |
| |
| |
| if HAS_FACE_RECOGNITION: |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) |
| face_locations = face_recognition.face_locations(gray) |
| if face_locations: |
| face_frames += len(face_locations) |
| |
| |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) |
| brightness_sum += np.mean(gray) / 255.0 |
| |
| |
| if prev_frame is not None: |
| flow = cv2.calcOpticalFlowFarneback(prev_frame, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0) |
| motion = np.mean(np.sqrt(flow[...,0]**2 + flow[...,1]**2)) |
| motion_sum += motion |
| prev_frame = gray |
| |
| cap.release() |
| |
| |
| results['nsfw_score'] = skin_tone_frames / sample_frames |
| results['faces_count'] = face_frames // sample_frames if sample_frames > 0 else 0 |
| results['brightness'] = brightness_sum / sample_frames if sample_frames > 0 else 0.5 |
| results['motion_score'] = motion_sum / (sample_frames - 1) if sample_frames > 1 else 0 |
| |
| |
| if results['faces_count'] > 2: |
| results['scene_type'] = 'group' |
| elif results['faces_count'] > 0: |
| results['scene_type'] = 'portrait' |
| elif results['motion_score'] > 10: |
| results['scene_type'] = 'action' |
| elif results['motion_score'] < 2: |
| results['scene_type'] = 'static' |
| else: |
| results['scene_type'] = 'normal' |
| |
| |
| tags = [] |
| if results['faces_count'] > 0: |
| tags.append('بشر') |
| tags.append('people') |
| if results['scene_type'] == 'action': |
| tags.append('حركة') |
| tags.append('action') |
| if results['brightness'] > 0.7: |
| tags.append('مشرق') |
| tags.append('bright') |
| elif results['brightness'] < 0.3: |
| tags.append('داكن') |
| tags.append('dark') |
| |
| results['tags'] = tags[:10] |
| |
| except Exception as e: |
| logger.error(f"خطأ في تحليل الفيديو: {e}") |
| |
| return results |
|
|
| def generate_ai_tags(text, video_analysis=None): |
| """توليد وسوم ذكية من النص وتحليل الفيديو""" |
| tags = set() |
| |
| |
| hashtags = re.findall(r'#(\w+)', text) |
| tags.update(hashtags) |
| |
| |
| words = re.findall(r'[\w]+', text) |
| important_words = [w for w in words if len(w) > 2 and w.isalpha()] |
| |
| common_tags = ['فيديو', 'ترند', 'مشاهير', 'جديد', 'حلويات', 'طبخ', |
| 'comedy', 'funny', 'music', 'dance', 'tiktok', 'fyp', |
| 'trending', 'viral', 'love', 'happy', 'sad', 'emotional'] |
| |
| for word in important_words: |
| word_lower = word.lower() |
| if word_lower in common_tags: |
| tags.add(word_lower) |
| |
| |
| if video_analysis: |
| tags.update(video_analysis.get('tags', [])) |
| |
| return json.dumps(list(tags)[:20]) |
|
|
| def moderate_comment(comment_text): |
| """فحص التعليقات تلقائياً""" |
| |
| banned_words = ['سب', 'شتم', 'سخاف', 'قذر', 'fuck', 'shit', 'ass', 'sex', 'porn'] |
| |
| comment_lower = comment_text.lower() |
| |
| |
| for word in banned_words: |
| if word in comment_lower: |
| return { |
| 'is_appropriate': False, |
| 'reason': f'يحتوي على كلمة ممنوعة: {word}', |
| 'action': 'block' |
| } |
| |
| |
| suspicious_patterns = [r'bit\.ly', r'tinyurl', r'goo\.gl', r'http://', r'https://'] |
| for pattern in suspicious_patterns: |
| if re.search(pattern, comment_lower): |
| return { |
| 'is_appropriate': False, |
| 'reason': 'يحتوي على رابط خارجي', |
| 'action': 'flag' |
| } |
| |
| |
| words = comment_lower.split() |
| if len(set(words)) < len(words) * 0.5 and len(words) > 10: |
| return { |
| 'is_appropriate': False, |
| 'reason': 'نسبة تكرار عالية (احتمال سبام)', |
| 'action': 'flag' |
| } |
| |
| return { |
| 'is_appropriate': True, |
| 'reason': None, |
| 'action': 'allow' |
| } |
|
|
| |
| def get_user_embedding(user_id): |
| """توليد تضمين متقدم للمستخدم""" |
| db = get_db() |
| |
| |
| interactions = db.execute(''' |
| SELECT v.vector, i.liked, i.watch_time, i.completed, i.shared, i.saved |
| FROM videos v |
| JOIN interactions i ON v.id = i.video_id |
| WHERE i.user_id = ? AND v.vector IS NOT NULL |
| ORDER BY i.timestamp DESC |
| LIMIT 100 |
| ''', (user_id,)).fetchall() |
| |
| if not interactions: |
| return np.zeros(VECTOR_DIM, dtype=np.float32) |
| |
| |
| vectors = [] |
| weights = [] |
| |
| for interaction in interactions: |
| vec = bytes_to_vector(interaction['vector']) |
| vectors.append(vec) |
| |
| |
| weight = 1.0 |
| if interaction['liked']: |
| weight += 2.0 |
| if interaction['completed']: |
| weight += 1.5 |
| if interaction['shared']: |
| weight += 2.5 |
| if interaction['saved']: |
| weight += 3.0 |
| |
| |
| watch_time = interaction['watch_time'] or 0 |
| weight += min(watch_time / 60, 2.0) |
| |
| weights.append(weight) |
| |
| if not vectors: |
| return np.zeros(VECTOR_DIM, dtype=np.float32) |
| |
| |
| vectors_array = np.array(vectors) |
| weights_array = np.array(weights).reshape(-1, 1) |
| weighted_avg = np.average(vectors_array, axis=0, weights=weights_array.flatten()) |
| |
| return weighted_avg |
|
|
| def get_trending_score(video_id): |
| """حساب درجة الشعبية للفيديو""" |
| db = get_db() |
| video = db.execute(''' |
| SELECT views, likes_count, comments_count, shares_count, saves_count, upload_time |
| FROM videos WHERE id = ? |
| ''', (video_id,)).fetchone() |
| |
| if not video: |
| return 0 |
| |
| |
| weights = { |
| 'view': 1, |
| 'like': 3, |
| 'comment': 5, |
| 'share': 8, |
| 'save': 4 |
| } |
| |
| |
| base_score = ( |
| video['views'] * weights['view'] + |
| video['likes_count'] * weights['like'] + |
| video['comments_count'] * weights['comment'] + |
| video['shares_count'] * weights['share'] + |
| video['saves_count'] * weights['save'] |
| ) |
| |
| |
| upload_time = datetime.strptime(video['upload_time'], '%Y-%m-%d %H:%M:%S') |
| hours_ago = (datetime.now() - upload_time).total_seconds() / 3600 |
| freshness = max(0, 100 / (hours_ago + 1)) |
| |
| |
| total_interactions = video['views'] + video['likes_count'] + video['comments_count'] |
| spread_factor = 1 + (video['shares_count'] / (total_interactions + 1)) |
| |
| final_score = base_score * freshness * spread_factor |
| return final_score |
|
|
| def update_trending(): |
| """تحديث المحتوى الرائج""" |
| global trending_cache |
| db = get_db() |
| |
| |
| 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.visibility = 'public' AND v.nsfw_score < 0.7 |
| ORDER BY |
| (v.views * 1 + v.likes_count * 3 + v.comments_count * 5 + v.shares_count * 8 + v.saves_count * 4) * |
| (1.0 / (julianday('now') - julianday(v.upload_time))) DESC |
| LIMIT 50 |
| ''').fetchall() |
| |
| |
| hashtags = db.execute(''' |
| SELECT h.*, |
| (h.usage_count * 1 + h.total_views * 0.1 + h.total_likes * 0.3) as trending_score |
| FROM hashtags h |
| ORDER BY trending_score DESC |
| LIMIT 20 |
| ''').fetchall() |
| |
| trending_cache = { |
| 'videos': [dict(v) for v in videos], |
| 'hashtags': [dict(h) for h in hashtags], |
| 'updated_at': datetime.now() |
| } |
| |
| return trending_cache |
|
|
| def get_trending(): |
| """الحصول على المحتوى الرائج""" |
| global trending_cache |
| |
| |
| if (trending_cache['updated_at'] and |
| datetime.now() - trending_cache['updated_at'] < timedelta(hours=1)): |
| return trending_cache |
| |
| return update_trending() |
|
|
| def recommend_videos_advanced(user_id, limit=10, offset=0, include_explore=False): |
| """نظام توصيات متقدم""" |
| 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 = 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] |
| |
| |
| user_embedding = get_user_embedding(user_id) |
| |
| |
| placeholders = ','.join(['?'] * len(watched_ids)) |
| |
| |
| query = f''' |
| SELECT |
| v.*, |
| u.username, |
| u.avatar, |
| u.is_verified, |
| u.role, |
| (SELECT COUNT(*) FROM follows WHERE user_id = v.user_id) as creator_followers, |
| (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, |
| julianday('now') - julianday(v.upload_time) as days_ago |
| FROM videos v |
| JOIN users u ON v.user_id = u.id |
| WHERE v.id NOT IN ({placeholders}) |
| AND v.visibility = 'public' |
| AND v.nsfw_score < 0.7 |
| ''' |
| |
| if not include_explore: |
| |
| query += ' AND (u.role IN ("creator", "vip") OR v.user_id IN ({follow_ph}))'.format( |
| follow_ph=','.join(['?'] * len(following_ids)) if following_ids else 'NULL' |
| ) |
| |
| query += ' ORDER BY v.upload_time DESC LIMIT ? OFFSET ?' |
| |
| params = watched_ids + (following_ids if following_ids else []) + [limit * 3, offset] |
| rows = db.execute(query, params).fetchall() |
| |
| video_scores = [] |
| for row in rows: |
| score = 0 |
| |
| |
| if not np.all(user_embedding == 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(user_embedding - vec)) |
| score += similarity * 30 |
| |
| |
| popularity = ( |
| row['views'] * 1 + |
| row['likes_count'] * 3 + |
| row['comments_count'] * 5 + |
| row['shares_count'] * 8 |
| ) / 1000 |
| score += min(popularity, 25) |
| |
| |
| days_ago = row['days_ago'] or 0 |
| recency = max(0, 20 - (days_ago * 2)) |
| score += recency |
| |
| |
| if row['user_id'] in following_ids: |
| score += 15 |
| elif row['creator_followers'] > 1000: |
| score += 5 |
| |
| |
| if row['views'] > 0: |
| engagement = (row['likes_count'] + row['comments_count']) / row['views'] * 100 |
| score += min(engagement, 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]] |
| |
| except Exception as e: |
| logger.error(f"خطأ في نظام التوصيات: {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' AND v.nsfw_score < 0.7 |
| ORDER BY RANDOM() |
| LIMIT ? OFFSET ? |
| ''', (limit, offset)).fetchall() |
| return [dict(row) for row in rows] |
|
|
| |
| def calculate_level_from_xp(xp): |
| """حساب المستوى من نقاط الخبرة (منحنى أسي)""" |
| if xp < 100: |
| return 1 |
| return int((xp / 100) ** 0.5) + 1 |
|
|
| def calculate_xp_for_level(level): |
| """حساب النقاط المطلوبة لمستوى معين""" |
| return 100 * (level - 1) ** 2 |
|
|
| def add_xp(user_id, action, base_xp, multiplier=1.0): |
| """إضافة نقاط خبرة مع مضاعفات""" |
| db = get_db() |
| try: |
| |
| xp_gained = int(base_xp * multiplier) |
| |
| db.execute(''' |
| INSERT INTO user_xp (user_id, action, xp_gained) |
| VALUES (?, ?, ?) |
| ''', (user_id, action, xp_gained)) |
| |
| db.execute('UPDATE users SET xp = xp + ? WHERE id = ?', (xp_gained, user_id)) |
| |
| |
| user = db.execute('SELECT xp, level FROM users WHERE id = ?', (user_id,)).fetchone() |
| if user: |
| new_level = calculate_level_from_xp(user['xp']) |
| if new_level > user['level']: |
| db.execute('UPDATE users SET level = ? WHERE id = ?', (new_level, user_id)) |
| |
| add_coins(user_id, new_level * 10, f'مكافأة وصول للمستوى {new_level}') |
| |
| |
| add_notification( |
| user_id, None, 'level_up', |
| f'تهانينا! وصلت إلى المستوى {new_level}', |
| priority=2 |
| ) |
| |
| db.commit() |
| return True |
| except Exception as e: |
| logger.error(f"خطأ في إضافة XP: {e}") |
| return False |
|
|
| def add_coins(user_id, amount, description, currency='coins'): |
| """إضافة عملات مع تسجيل المعاملة""" |
| db = get_db() |
| try: |
| |
| user = db.execute(f'SELECT {currency} FROM users WHERE id = ?', (user_id,)).fetchone() |
| balance_before = user[currency] if user else 0 |
| |
| |
| db.execute(f'UPDATE users SET {currency} = {currency} + ? WHERE id = ?', (amount, user_id)) |
| |
| |
| db.execute(''' |
| INSERT INTO transactions (user_id, type, amount, currency, balance_after, description) |
| VALUES (?, 'earn', ?, ?, ?, ?) |
| ''', (user_id, amount, currency, balance_before + amount, description)) |
| |
| db.commit() |
| return True |
| except Exception as e: |
| logger.error(f"خطأ في إضافة العملات: {e}") |
| return False |
|
|
| def deduct_coins(user_id, amount, description, currency='coins'): |
| """خصم عملات مع تسجيل المعاملة""" |
| db = get_db() |
| try: |
| |
| user = db.execute(f'SELECT {currency} FROM users WHERE id = ?', (user_id,)).fetchone() |
| if not user or user[currency] < amount: |
| return False |
| |
| balance_before = user[currency] |
| |
| |
| db.execute(f'UPDATE users SET {currency} = {currency} - ? WHERE id = ?', (amount, user_id)) |
| |
| |
| db.execute(''' |
| INSERT INTO transactions (user_id, type, amount, currency, balance_after, description) |
| VALUES (?, 'spend', ?, ?, ?, ?) |
| ''', (user_id, amount, currency, balance_before - amount, description)) |
| |
| db.commit() |
| return True |
| except Exception as e: |
| logger.error(f"خطأ في خصم العملات: {e}") |
| return False |
|
|
| 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() |
| |
| 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 |
| |
| |
| base_coins = 50 |
| base_xp = 20 |
| |
| coins = base_coins * streak |
| xp = base_xp * streak |
| |
| |
| bonus_coins = 0 |
| if streak >= 7: |
| bonus_coins += 100 |
| if streak >= 30: |
| bonus_coins += 500 |
| |
| total_coins = coins + bonus_coins |
| |
| |
| db.execute(''' |
| INSERT INTO daily_rewards (user_id, reward_date, reward_coins, reward_xp, streak) |
| VALUES (?, ?, ?, ?, ?) |
| ''', (user_id, today.isoformat(), total_coins, xp, streak)) |
| |
| |
| add_coins(user_id, total_coins, f'مكافأة يومية (السلسلة: {streak})') |
| add_xp(user_id, 'daily_reward', xp) |
| |
| db.commit() |
| return True, total_coins, xp, streak |
| |
| except Exception as e: |
| logger.error(f"خطأ في المكافأة اليومية: {e}") |
| return False, str(e) |
|
|
| |
| def generate_2fa_secret(): |
| """توليد سر للمصادقة الثنائية""" |
| if not HAS_2FA: |
| return None |
| return pyotp.random_base32() |
|
|
| def generate_2fa_qr(username, secret): |
| """توليد رمز QR للمصادقة الثنائية""" |
| if not HAS_2FA: |
| return None |
| |
| totp = pyotp.TOTP(secret) |
| provisioning_uri = totp.provisioning_uri(name=username, issuer_name="ARC Video") |
| |
| |
| qr = qrcode.QRCode(version=1, box_size=10, border=5) |
| qr.add_data(provisioning_uri) |
| qr.make(fit=True) |
| |
| img = qr.make_image(fill_color="black", back_color="white") |
| |
| |
| buffered = BytesIO() |
| img.save(buffered, format="PNG") |
| img_str = base64.b64encode(buffered.getvalue()).decode() |
| |
| return img_str |
|
|
| def verify_2fa_code(secret, code): |
| """التحقق من رمز المصادقة الثنائية""" |
| if not HAS_2FA or not secret: |
| return False |
| totp = pyotp.TOTP(secret) |
| return totp.verify(code) |
|
|
| def generate_backup_codes(): |
| """توليد رموز احتياطية""" |
| codes = [] |
| for _ in range(8): |
| code = secrets.token_hex(4).upper() |
| codes.append(code) |
| return json.dumps(codes) |
|
|
| def encrypt_sensitive_data(data, key=None): |
| """تشفير البيانات الحساسة""" |
| if not HAS_CRYPTO: |
| return data |
| |
| if key is None: |
| key = MASTER_ENCRYPTION_KEY |
| |
| try: |
| if isinstance(data, str): |
| data = data.encode('utf-8') |
| |
| cipher = AES.new(key, AES.MODE_GCM) |
| ciphertext, tag = cipher.encrypt_and_digest(data) |
| |
| return { |
| 'ciphertext': base64.b64encode(ciphertext).decode('utf-8'), |
| 'nonce': base64.b64encode(cipher.nonce).decode('utf-8'), |
| 'tag': base64.b64encode(tag).decode('utf-8') |
| } |
| except Exception as e: |
| logger.error(f"خطأ في التشفير: {e}") |
| return data |
|
|
| def decrypt_sensitive_data(encrypted_data, key=None): |
| """فك تشفير البيانات الحساسة""" |
| if not HAS_CRYPTO: |
| return encrypted_data |
| |
| if key is None: |
| key = MASTER_ENCRYPTION_KEY |
| |
| try: |
| if isinstance(encrypted_data, dict) and 'ciphertext' in encrypted_data: |
| ciphertext = base64.b64decode(encrypted_data['ciphertext']) |
| nonce = base64.b64decode(encrypted_data['nonce']) |
| tag = base64.b64decode(encrypted_data['tag']) |
| |
| cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) |
| data = cipher.decrypt_and_verify(ciphertext, tag) |
| return data.decode('utf-8') |
| except Exception as e: |
| logger.error(f"خطأ في فك التشفير: {e}") |
| |
| return encrypted_data |
|
|
| |
| def add_notification(user_id, from_user_id, n_type, content, |
| video_id=None, comment_id=None, gift_id=None, |
| priority=0, action_url=None, image_url=None): |
| """إضافة إشعار متقدم""" |
| if user_id == from_user_id: |
| return |
| |
| db = get_db() |
| |
| |
| expires_at = datetime.now() + timedelta(days=30) |
| |
| cursor = db.execute(''' |
| INSERT INTO notifications ( |
| user_id, from_user_id, type, content, video_id, |
| comment_id, gift_id, priority, action_url, image_url, expires_at |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| ''', (user_id, from_user_id, n_type, content, video_id, |
| comment_id, gift_id, priority, action_url, image_url, expires_at)) |
| |
| db.commit() |
| |
| |
| if user_id in notification_queues: |
| notification_queues[user_id].put({ |
| 'id': cursor.lastrowid, |
| 'type': n_type, |
| 'content': content, |
| 'from_user': from_user_id, |
| 'video_id': video_id, |
| 'priority': priority, |
| 'image_url': image_url, |
| 'timestamp': datetime.now().isoformat() |
| }) |
| |
| return cursor.lastrowid |
|
|
| def send_bulk_notifications(user_ids, from_user_id, n_type, content, **kwargs): |
| """إرسال إشعارات جماعية""" |
| for user_id in user_ids: |
| add_notification(user_id, from_user_id, n_type, content, **kwargs) |
|
|
| |
| def create_developer_session(user_id): |
| """إنشاء جلسة مطور جديدة""" |
| token = secrets.token_urlsafe(48) |
| expires = datetime.now() + timedelta(hours=2) |
| 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 > datetime('now') |
| ''', (token,)).fetchone() |
| |
| if session: |
| return session['user_id'] |
| return None |
|
|
| |
| def login_required(f): |
| @wraps(f) |
| def decorated_function(*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 |
|
|
| def admin_required(f): |
| @wraps(f) |
| def decorated_function(*args, **kwargs): |
| if 'user_id' not in session: |
| return redirect(url_for('login')) |
| user = query_db('SELECT role FROM users WHERE id = ?', (session['user_id'],), one=True) |
| if not user or user['role'] not in ['admin', 'moderator']: |
| abort(403) |
| 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')) |
| user = query_db('SELECT is_developer FROM users WHERE id = ?', (session['user_id'],), one=True) |
| if not user or not user['is_developer']: |
| abort(403) |
| return f(*args, **kwargs) |
| return decorated_function |
|
|
| def vip_required(f): |
| @wraps(f) |
| def decorated_function(*args, **kwargs): |
| if 'user_id' not in session: |
| return redirect(url_for('login')) |
| tier = get_user_membership_tier(session['user_id']) |
| if tier in [MembershipTier.VIP, MembershipTier.VIP_GOLD]: |
| return f(*args, **kwargs) |
| abort(403) |
| return decorated_function |
|
|
| |
| @app.route('/') |
| def index(): |
| if 'user_id' not in session: |
| return redirect(url_for('login')) |
| return render_template_string(MAIN_TEMPLATE, session=session) |
|
|
| @app.route('/login', methods=['GET', 'POST']) |
| @limiter.limit("10 per minute") |
| def login(): |
| if request.method == 'POST': |
| username = request.form.get('username', '').strip() |
| password = request.form.get('password', '') |
| remember = request.form.get('remember', False) |
| twofa_code = request.form.get('twofa_code', '') |
| |
| db = get_db() |
| user = db.execute('SELECT * FROM users WHERE username = ? OR email = ?', |
| (username, username)).fetchone() |
| |
| if not user: |
| return render_template_string(LOGIN_PAGE, error='بيانات الدخول غير صحيحة') |
| |
| |
| if user['locked_until']: |
| locked_until = datetime.strptime(user['locked_until'], '%Y-%m-%d %H:%M:%S') |
| if locked_until > datetime.now(): |
| return render_template_string(LOGIN_PAGE, |
| error=f'الحساب مقفل حتى {locked_until.strftime("%H:%M")}') |
| |
| if check_password_hash(user['password_hash'], password): |
| |
| if user['two_factor_enabled'] and not twofa_code: |
| return render_template_string(TWOFA_PAGE, user_id=user['id']) |
| |
| if user['two_factor_enabled']: |
| if not verify_2fa_code(user['two_factor_secret'], twofa_code): |
| return render_template_string(LOGIN_PAGE, error='رمز المصادقة غير صحيح') |
| |
| |
| db.execute('UPDATE users SET login_attempts = 0, locked_until = NULL WHERE id = ?', |
| (user['id'],)) |
| |
| |
| session_id = secrets.token_urlsafe(32) |
| session['user_id'] = user['id'] |
| session['username'] = user['username'] |
| session['role'] = user['role'] |
| session['session_id'] = session_id |
| |
| if remember: |
| session.permanent = True |
| |
| |
| db.execute(''' |
| INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at) |
| VALUES (?, ?, ?, ?, datetime("now", "+30 days")) |
| ''', (session_id, user['id'], get_client_ip(), request.headers.get('User-Agent', ''))) |
| |
| db.execute('UPDATE users SET last_active = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],)) |
| db.commit() |
| |
| |
| cache_key = f"login_{user['id']}_{datetime.now().date()}" |
| if not cache_get(cache_key): |
| add_xp(user['id'], 'daily_login', 10) |
| cache_set(cache_key, '1', 86400) |
| |
| track_event(user['id'], 'login', {}) |
| |
| if request.headers.get('X-Requested-With') == 'XMLHttpRequest': |
| return jsonify({'success': True, 'redirect': '/'}) |
| return redirect(url_for('index')) |
| |
| |
| attempts = user['login_attempts'] + 1 |
| locked_until = None |
| if attempts >= 5: |
| locked_until = datetime.now() + timedelta(minutes=15) |
| |
| db.execute('UPDATE users SET login_attempts = ?, locked_until = ? WHERE id = ?', |
| (attempts, locked_until, user['id'])) |
| db.commit() |
| |
| 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']) |
| @limiter.limit("5 per hour") |
| def register(): |
| if request.method == 'POST': |
| username = request.form.get('username', '').strip() |
| email = request.form.get('email', '').strip().lower() |
| password = request.form.get('password', '') |
| confirm = request.form.get('confirm_password', '') |
| referral = request.form.get('referral_code', '').strip() |
| agree_terms = request.form.get('agree_terms') == 'on' |
| |
| if not username or not email or not password: |
| return render_template_string(REGISTER_PAGE, error='جميع الحقول مطلوبة') |
| |
| if not agree_terms: |
| return render_template_string(REGISTER_PAGE, error='يجب الموافقة على الشروط والأحكام') |
| |
| if password != confirm: |
| return render_template_string(REGISTER_PAGE, error='كلمات المرور غير متطابقة') |
| |
| if len(password) < 8: |
| return render_template_string(REGISTER_PAGE, |
| error='كلمة المرور يجب أن تكون 8 أحرف على الأقل') |
| |
| if not re.search(r'[A-Z]', password): |
| return render_template_string(REGISTER_PAGE, |
| error='كلمة المرور يجب أن تحتوي على حرف كبير واحد على الأقل') |
| |
| if not re.search(r'[a-z]', password): |
| return render_template_string(REGISTER_PAGE, |
| error='كلمة المرور يجب أن تحتوي على حرف صغير واحد على الأقل') |
| |
| if not re.search(r'[0-9]', password): |
| 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='اسم المستخدم أو البريد الإلكتروني موجود بالفعل') |
| |
| hashed = generate_password_hash(password) |
| referral_code = generate_referral_code() |
| |
| cursor = db.execute(''' |
| INSERT INTO users (username, email, password_hash, referral_code) |
| VALUES (?, ?, ?, ?) |
| ''', (username, email, hashed, referral_code)) |
| |
| user_id = cursor.lastrowid |
| 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) |
| |
| |
| add_coins(user_id, 100, 'هدية ترحيبية') |
| |
| track_event(user_id, 'register', {'referral': referral if referral else None}) |
| |
| db.commit() |
| |
| |
| logger.info(f"مستخدم جديد: {username} (ID: {user_id})") |
| |
| return redirect(url_for('login')) |
| |
| return render_template_string(REGISTER_PAGE) |
|
|
| @app.route('/logout') |
| def logout(): |
| if 'user_id' in session and 'session_id' in session: |
| db = get_db() |
| db.execute('DELETE FROM sessions WHERE id = ?', (session['session_id'],)) |
| db.commit() |
| |
| track_event(session['user_id'], 'logout', {}) |
| |
| session.clear() |
| return redirect(url_for('login')) |
|
|
| @app.route('/2fa/setup', methods=['GET', 'POST']) |
| @login_required |
| def setup_2fa(): |
| """إعداد المصادقة الثنائية""" |
| if not HAS_2FA: |
| return "المصادقة الثنائية غير متاحة", 501 |
| |
| db = get_db() |
| user = db.execute('SELECT username, two_factor_secret FROM users WHERE id = ?', |
| (session['user_id'],)).fetchone() |
| |
| if request.method == 'POST': |
| code = request.form.get('code', '') |
| if verify_2fa_code(user['two_factor_secret'], code): |
| db.execute('UPDATE users SET two_factor_enabled = 1 WHERE id = ?', |
| (session['user_id'],)) |
| db.commit() |
| return redirect(url_for('settings')) |
| else: |
| return render_template_string(SETUP_2FA_PAGE, error='رمز غير صحيح') |
| |
| |
| secret = user['two_factor_secret'] |
| if not secret: |
| secret = generate_2fa_secret() |
| db.execute('UPDATE users SET two_factor_secret = ? WHERE id = ?', |
| (secret, session['user_id'])) |
| db.commit() |
| |
| qr_code = generate_2fa_qr(user['username'], secret) |
| backup_codes = generate_backup_codes() |
| db.execute('UPDATE users SET backup_codes = ? WHERE id = ?', |
| (backup_codes, session['user_id'])) |
| db.commit() |
| |
| return render_template_string(SETUP_2FA_PAGE, |
| secret=secret, |
| qr_code=qr_code, |
| backup_codes=json.loads(backup_codes)) |
|
|
| |
| @app.route('/profile/<int:user_id>') |
| @login_required |
| def profile(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() |
| |
| if not user: |
| abort(404) |
| |
| |
| stats = get_user_stats(user_id) |
| |
| |
| videos = db.execute(''' |
| SELECT id, filename, title, thumbnail, 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 |
| |
| |
| videos_list = [] |
| for v in videos: |
| video_dict = dict(v) |
| video_dict['url'] = v['cloudinary_url'] if v['cloudinary_url'] else f'/videos/{v["filename"]}' |
| videos_list.append(video_dict) |
| |
| |
| benefits = get_membership_benefits(user_id) |
| |
| return render_template_string(PROFILE_TEMPLATE, |
| user=dict(user), |
| videos=videos_list, |
| stats=stats, |
| followers_count=followers_count, |
| following_count=following_count, |
| is_following=is_following, |
| benefits=benefits, |
| session=session) |
|
|
| @app.route('/settings', methods=['GET', 'POST']) |
| @login_required |
| def settings(): |
| db = get_db() |
| |
| if request.method == 'POST': |
| |
| bio = request.form.get('bio', '') |
| phone = request.form.get('phone', '') |
| gender = request.form.get('gender', '') |
| country = request.form.get('country', '') |
| birth_date = request.form.get('birth_date', '') |
| |
| |
| avatar = request.files.get('avatar') |
| cover = request.files.get('cover') |
| |
| if avatar and avatar.filename and allowed_image(avatar.filename): |
| ext = avatar.filename.rsplit('.', 1)[1].lower() |
| filename = f"avatar_{session['user_id']}_{uuid.uuid4().hex[:8]}.{ext}" |
| filepath = os.path.join(AVATAR_FOLDER, filename) |
| avatar.save(filepath) |
| |
| |
| if HAS_PIL: |
| try: |
| img = Image.open(filepath) |
| img = img.resize((400, 400), Image.Resampling.LANCZOS) |
| img.save(filepath, optimize=True, quality=85) |
| except: |
| pass |
| |
| db.execute('UPDATE users SET avatar = ? WHERE id = ?', (filename, session['user_id'])) |
| |
| if cover and cover.filename and allowed_image(cover.filename): |
| ext = cover.filename.rsplit('.', 1)[1].lower() |
| filename = f"cover_{session['user_id']}_{uuid.uuid4().hex[:8]}.{ext}" |
| filepath = os.path.join(AVATAR_FOLDER, filename) |
| cover.save(filepath) |
| |
| if HAS_PIL: |
| try: |
| img = Image.open(filepath) |
| img = img.resize((1500, 500), Image.Resampling.LANCZOS) |
| img.save(filepath, optimize=True, quality=85) |
| except: |
| pass |
| |
| db.execute('UPDATE users SET cover_image = ? WHERE id = ?', (filename, session['user_id'])) |
| |
| db.execute(''' |
| UPDATE users SET bio = ?, phone = ?, gender = ?, country = ?, birth_date = ? |
| WHERE id = ? |
| ''', (bio, phone, gender, country, birth_date, session['user_id'])) |
| |
| |
| privacy_profile = request.form.get('privacy_profile', 'public') |
| privacy_videos = request.form.get('privacy_videos', 'public') |
| privacy_likes = request.form.get('privacy_likes', 'public') |
| |
| |
| notifications_likes = request.form.get('notifications_likes') == 'on' |
| notifications_comments = request.form.get('notifications_comments') == 'on' |
| notifications_follows = request.form.get('notifications_follows') == 'on' |
| notifications_messages = request.form.get('notifications_messages') == 'on' |
| notifications_live = request.form.get('notifications_live') == 'on' |
| notifications_gifts = request.form.get('notifications_gifts') == 'on' |
| |
| |
| dark_mode = request.form.get('dark_mode') == 'on' |
| language = request.form.get('language', 'ar') |
| content_language = request.form.get('content_language', 'ar') |
| autoplay = request.form.get('autoplay') == 'on' |
| save_data = request.form.get('save_data') == 'on' |
| allow_download = request.form.get('allow_download') == 'on' |
| allow_duet = request.form.get('allow_duet') == 'on' |
| allow_stitch = request.form.get('allow_stitch') == 'on' |
| |
| db.execute(''' |
| UPDATE user_settings SET |
| privacy_profile = ?, privacy_videos = ?, privacy_likes = ?, |
| notifications_likes = ?, notifications_comments = ?, |
| notifications_follows = ?, notifications_messages = ?, |
| notifications_live = ?, notifications_gifts = ?, |
| dark_mode = ?, language = ?, content_language = ?, |
| autoplay = ?, save_data = ?, allow_download = ?, |
| allow_duet = ?, allow_stitch = ? |
| WHERE user_id = ? |
| ''', (privacy_profile, privacy_videos, privacy_likes, |
| notifications_likes, notifications_comments, |
| notifications_follows, notifications_messages, |
| notifications_live, notifications_gifts, |
| dark_mode, language, content_language, |
| autoplay, save_data, allow_download, |
| allow_duet, allow_stitch, session['user_id'])) |
| |
| db.commit() |
| |
| track_event(session['user_id'], 'update_settings', {}) |
| |
| return redirect(url_for('profile', user_id=session['user_id'])) |
| |
| user = db.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone() |
| settings = db.execute('SELECT * FROM user_settings WHERE user_id = ?', |
| (session['user_id'],)).fetchone() |
| |
| return render_template_string(SETTINGS_TEMPLATE, |
| user=dict(user), |
| settings=dict(settings) if settings else {}) |
|
|
| 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 |
| total_shares = db.execute('SELECT SUM(shares_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() |
| |
| |
| weekly_views = db.execute(''' |
| SELECT SUM(watch_time) FROM video_views |
| WHERE user_id = ? AND viewed_at > datetime("now", "-7 days") |
| ''', (user_id,)).fetchone()[0] or 0 |
| |
| |
| achievements = [] |
| if videos >= 10: |
| achievements.append('منشئ محتوى') |
| if followers >= 1000: |
| achievements.append('مؤثر') |
| if total_views >= 100000: |
| achievements.append('نجم') |
| |
| return { |
| 'videos': videos, |
| 'total_views': total_views, |
| 'total_likes': total_likes, |
| 'total_comments': total_comments, |
| 'total_shares': total_shares, |
| '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'], |
| 'weekly_views': weekly_views, |
| 'achievements': achievements |
| } |
| except Exception as e: |
| logger.error(f"خطأ في إحصائيات المستخدم: {e}") |
| return {} |
|
|
| |
| @app.route('/follow/<int:user_id>', methods=['POST']) |
| @login_required |
| @limiter.limit("30 per minute") |
| def follow(user_id): |
| if user_id == session['user_id']: |
| return jsonify({'error': 'لا يمكنك متابعة نفسك'}), 400 |
| |
| db = get_db() |
| |
| |
| target_user = db.execute('SELECT username FROM users WHERE id = ?', (user_id,)).fetchone() |
| if not target_user: |
| return jsonify({'error': 'المستخدم غير موجود'}), 404 |
| |
| db.execute('INSERT OR IGNORE INTO follows (user_id, follower_id) VALUES (?, ?)', |
| (user_id, session['user_id'])) |
| |
| |
| db.execute(''' |
| UPDATE users SET total_followers = ( |
| SELECT COUNT(*) FROM follows WHERE user_id = ? |
| ) WHERE id = ? |
| ''', (user_id, user_id)) |
| |
| db.execute(''' |
| UPDATE users SET total_following = ( |
| SELECT COUNT(*) FROM follows WHERE follower_id = ? |
| ) WHERE id = ? |
| ''', (session['user_id'], session['user_id'])) |
| |
| db.commit() |
| |
| |
| follower = db.execute('SELECT username FROM users WHERE id = ?', |
| (session['user_id'],)).fetchone() |
| add_notification(user_id, session['user_id'], 'follow', |
| f'بدأ {follower["username"]} بمتابعتك', |
| priority=1) |
| |
| |
| add_xp(session['user_id'], 'follow', 5) |
| |
| |
| track_event(session['user_id'], 'follow', {'followed_user_id': user_id}) |
| |
| count = db.execute('SELECT COUNT(*) FROM follows WHERE user_id = ?', |
| (user_id,)).fetchone()[0] |
| |
| return jsonify({'status': 'ok', 'followers_count': count}) |
|
|
| @app.route('/unfollow/<int:user_id>', 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(''' |
| UPDATE users SET total_followers = ( |
| SELECT COUNT(*) FROM follows WHERE user_id = ? |
| ) WHERE id = ? |
| ''', (user_id, user_id)) |
| |
| db.execute(''' |
| UPDATE users SET total_following = ( |
| SELECT COUNT(*) FROM follows WHERE follower_id = ? |
| ) WHERE id = ? |
| ''', (session['user_id'], session['user_id'])) |
| |
| db.commit() |
| |
| track_event(session['user_id'], 'unfollow', {'unfollowed_user_id': user_id}) |
| |
| 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/<int:user_id>') |
| @login_required |
| def get_followers(user_id): |
| page = int(request.args.get('page', 1)) |
| limit = 20 |
| offset = (page - 1) * limit |
| |
| db = get_db() |
| followers = db.execute(''' |
| SELECT u.id, u.username, u.avatar, u.is_verified, u.role, |
| f.created_at |
| FROM follows f |
| JOIN users u ON f.follower_id = u.id |
| WHERE f.user_id = ? |
| ORDER BY f.created_at DESC |
| LIMIT ? OFFSET ? |
| ''', (user_id, limit, offset)).fetchall() |
| |
| result = [] |
| for f in followers: |
| follower = dict(f) |
| follower['avatar_url'] = f'/avatars/{f["avatar"]}' |
| |
| |
| if session['user_id'] != f['id']: |
| is_following_back = db.execute(''' |
| SELECT * FROM follows WHERE user_id = ? AND follower_id = ? |
| ''', (f['id'], session['user_id'])).fetchone() |
| follower['is_following_back'] = is_following_back is not None |
| else: |
| follower['is_following_back'] = False |
| |
| result.append(follower) |
| |
| return jsonify({ |
| 'followers': result, |
| 'has_more': len(result) == limit |
| }) |
|
|
| @app.route('/api/following/<int:user_id>') |
| @login_required |
| def get_following(user_id): |
| page = int(request.args.get('page', 1)) |
| limit = 20 |
| offset = (page - 1) * limit |
| |
| db = get_db() |
| following = db.execute(''' |
| SELECT u.id, u.username, u.avatar, u.is_verified, u.role, |
| f.created_at |
| FROM follows f |
| JOIN users u ON f.user_id = u.id |
| WHERE f.follower_id = ? |
| ORDER BY f.created_at DESC |
| LIMIT ? OFFSET ? |
| ''', (user_id, limit, offset)).fetchall() |
| |
| result = [] |
| for f in following: |
| follow = dict(f) |
| follow['avatar_url'] = f'/avatars/{f["avatar"]}' |
| result.append(follow) |
| |
| return jsonify({ |
| 'following': result, |
| 'has_more': len(result) == limit |
| }) |
|
|
| |
| @app.route('/upload', methods=['GET', 'POST']) |
| @login_required |
| def upload_video(): |
| if request.method == 'GET': |
| db = get_db() |
| challenges = db.execute(''' |
| SELECT * FROM challenges |
| WHERE is_active = 1 AND end_date > DATE() |
| ORDER BY prize_coins DESC |
| ''').fetchall() |
| |
| music = db.execute(''' |
| SELECT * FROM music_tracks WHERE is_active = 1 |
| ORDER BY usage_count DESC |
| LIMIT 50 |
| ''').fetchall() |
| |
| return render_template_string(UPLOAD_TEMPLATE, |
| challenges=[dict(c) for c in challenges], |
| music=[dict(m) for m in music]) |
| |
| |
| if 'video' not in request.files: |
| return jsonify({'error': 'لم يتم اختيار فيديو'}), 400 |
| |
| file = request.files['video'] |
| if file.filename == '': |
| return jsonify({'error': 'لم يتم اختيار فيديو'}), 400 |
| |
| |
| benefits = get_membership_benefits(session['user_id']) |
| |
| |
| file.seek(0, os.SEEK_END) |
| file_size = file.tell() |
| file.seek(0) |
| |
| if file_size > benefits.max_video_size: |
| max_size_mb = benefits.max_video_size / (1024 * 1024) |
| return jsonify({'error': f'حجم الملف يتجاوز الحد المسموح ({max_size_mb}MB)'}), 400 |
| |
| title = request.form.get('title', '') |
| description = request.form.get('description', '') |
| music_id = request.form.get('music_id') |
| 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}" |
| filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_name) |
| file.save(filepath) |
| |
| |
| cloudinary_url, cloudinary_public_id = upload_video_to_cloudinary(filepath) |
| thumbnail_url = generate_video_thumbnail(cloudinary_public_id) if cloudinary_public_id else None |
| |
| |
| video_analysis = analyze_video_content(filepath) |
| |
| |
| if HAS_CV2: |
| vec_bytes = extract_video_features(filepath) |
| else: |
| vec_bytes = random_vector() |
| |
| |
| ai_tags = generate_ai_tags(f"{title} {description}", video_analysis) |
| |
| |
| encrypted_path, enc_key = None, None |
| if HAS_CRYPTO and benefits.priority_support: |
| encrypted_path, enc_key = encrypt_video_file(filepath) |
| |
| |
| watermarked_path = None |
| if HAS_CV2 and HAS_PIL: |
| watermark_text = f"@{session['username']}" |
| watermarked_path = add_watermark_to_video(filepath, watermark_text=watermark_text) |
| |
| db = get_db() |
| cursor = db.execute(''' |
| INSERT INTO videos ( |
| user_id, filename, filepath, filesize, cloudinary_url, |
| cloudinary_public_id, thumbnail, title, description, music_id, |
| allow_comments, allow_duet, allow_stitch, visibility, |
| vector, challenge_id, nsfw_score, ai_tags, |
| encrypted_path, encrypted_key, watermarked_path |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| ''', (session['user_id'], unique_name, filepath, file_size, |
| cloudinary_url, cloudinary_public_id, thumbnail_url, |
| title, description, music_id, |
| allow_comments, allow_duet, allow_stitch, visibility, |
| vec_bytes, challenge_id, video_analysis['nsfw_score'], |
| ai_tags, encrypted_path, enc_key, watermarked_path)) |
| |
| 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'],)) |
| |
| |
| 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() |
| |
| |
| add_coins(session['user_id'], 20, 'مكافأة نشر فيديو') |
| add_xp(session['user_id'], 'upload_video', 50, |
| multiplier=2 if benefits.priority_support else 1) |
| |
| track_event(session['user_id'], 'upload_video', { |
| 'video_id': video_id, |
| 'size': file_size, |
| 'duration': video_analysis.get('duration', 0) |
| }) |
| |
| return redirect(url_for('index')) |
| |
| return jsonify({'error': 'نوع الملف غير مدعوم'}), 400 |
|
|
| @app.route('/api/for-you') |
| @login_required |
| def for_you(): |
| """الصفحة الرئيسية مع توصيات متقدمة""" |
| try: |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 10)) |
| offset = (page - 1) * limit |
| |
| |
| videos = recommend_videos_advanced(session['user_id'], limit, offset) |
| |
| result = [] |
| for v in videos: |
| video = {} |
| for key, value in v.items(): |
| if key not in ['vector', 'encrypted_key'] and not isinstance(value, bytes): |
| video[key] = value |
| |
| video['url'] = v['cloudinary_url'] if v['cloudinary_url'] else f'/videos/{v["filename"]}' |
| video['avatar_url'] = f'/avatars/{v["avatar"]}' |
| |
| |
| db = get_db() |
| interaction = db.execute(''' |
| SELECT liked, saved 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 |
| video['saved_by_user'] = bool(interaction['saved']) if interaction else False |
| |
| result.append(video) |
| |
| |
| track_event(session['user_id'], 'view_for_you', {'page': page}) |
| |
| return jsonify({ |
| 'videos': result, |
| 'next_page': page + 1 if len(result) == limit else None, |
| 'has_more': len(result) == limit |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في for_you: {e}") |
| return jsonify({'videos': [], 'next_page': None, 'error': str(e)}), 500 |
|
|
| @app.route('/api/trending') |
| @login_required |
| def trending(): |
| """الفيديوهات الرائجة""" |
| try: |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 10)) |
| offset = (page - 1) * limit |
| |
| trending_data = get_trending() |
| videos = trending_data['videos'][offset:offset+limit] |
| |
| result = [] |
| for v in videos: |
| video = dict(v) |
| video['url'] = v['cloudinary_url'] if v['cloudinary_url'] else f'/videos/{v["filename"]}' |
| video['avatar_url'] = f'/avatars/{v["avatar"]}' |
| video.pop('vector', None) |
| video.pop('encrypted_key', None) |
| |
| result.append(video) |
| |
| return jsonify({ |
| 'videos': result, |
| 'next_page': page + 1 if len(result) == limit else None, |
| 'has_more': len(videos) == limit |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في trending: {e}") |
| return jsonify({'videos': [], 'next_page': None}), 500 |
|
|
| @app.route('/api/trending/hashtags') |
| @login_required |
| def trending_hashtags(): |
| """الهاشتاغات الرائجة""" |
| try: |
| trending_data = get_trending() |
| return jsonify({'hashtags': trending_data['hashtags']}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في trending_hashtags: {e}") |
| return jsonify({'hashtags': []}), 500 |
|
|
| @app.route('/api/search') |
| @login_required |
| def search(): |
| """بحث متقدم""" |
| try: |
| q = request.args.get('q', '').strip() |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 20)) |
| offset = (page - 1) * limit |
| content_type = request.args.get('type', 'videos') |
| |
| db = get_db() |
| |
| |
| if q: |
| db.execute('INSERT INTO search_queries (user_id, query) VALUES (?, ?)', |
| (session['user_id'], q)) |
| db.commit() |
| |
| if content_type == 'videos': |
| |
| 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' |
| AND (v.title LIKE ? OR v.description LIKE ?) |
| ORDER BY v.views DESC, v.likes_count DESC |
| LIMIT ? OFFSET ? |
| ''', ('%'+q+'%', '%'+q+'%', limit, offset)).fetchall() |
| |
| |
| hashtag_videos = [] |
| hashtag = db.execute('SELECT id FROM hashtags WHERE tag LIKE ?', |
| ('%'+q+'%',)).fetchone() |
| if hashtag: |
| hashtag_videos = db.execute(''' |
| SELECT v.*, u.username, u.avatar, u.is_verified |
| 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 ? |
| ''', (hashtag['id'], limit)).fetchall() |
| |
| |
| all_videos = list(videos) + list(hashtag_videos) |
| seen = set() |
| unique_videos = [] |
| for v in all_videos: |
| if v['id'] not in seen: |
| seen.add(v['id']) |
| unique_videos.append(v) |
| |
| result = [] |
| for v in unique_videos[:limit]: |
| video = dict(v) |
| video['url'] = v['cloudinary_url'] if v['cloudinary_url'] else f'/videos/{v["filename"]}' |
| video['avatar_url'] = f'/avatars/{v["avatar"]}' |
| video.pop('vector', None) |
| result.append(video) |
| |
| track_event(session['user_id'], 'search', {'query': q, 'results': len(result)}) |
| |
| return jsonify({ |
| 'videos': result, |
| 'next_page': page + 1 if len(result) == limit else None, |
| 'has_more': len(result) == limit |
| }) |
| |
| elif content_type == 'users': |
| |
| users = db.execute(''' |
| SELECT id, username, avatar, bio, is_verified, role, |
| total_followers |
| FROM users |
| WHERE username LIKE ? OR bio LIKE ? |
| ORDER BY total_followers DESC |
| LIMIT ? OFFSET ? |
| ''', ('%'+q+'%', '%'+q+'%', limit, offset)).fetchall() |
| |
| result = [] |
| for u in users: |
| user = dict(u) |
| user['avatar_url'] = f'/avatars/{u["avatar"]}' |
| |
| |
| if session['user_id'] != u['id']: |
| is_following = db.execute(''' |
| SELECT * FROM follows WHERE user_id = ? AND follower_id = ? |
| ''', (u['id'], session['user_id'])).fetchone() |
| user['is_following'] = is_following is not None |
| |
| result.append(user) |
| |
| return jsonify({ |
| 'users': result, |
| 'next_page': page + 1 if len(result) == limit else None |
| }) |
| |
| elif content_type == 'hashtags': |
| |
| hashtags = db.execute(''' |
| SELECT tag, usage_count, total_views |
| FROM hashtags |
| WHERE tag LIKE ? |
| ORDER BY usage_count DESC, total_views DESC |
| LIMIT ? OFFSET ? |
| ''', ('%'+q+'%', limit, offset)).fetchall() |
| |
| return jsonify({ |
| 'hashtags': [dict(h) for h in hashtags], |
| 'next_page': page + 1 if len(hashtags) == limit else None |
| }) |
| |
| return jsonify({'error': 'نوع بحث غير صحيح'}), 400 |
| |
| except Exception as e: |
| logger.error(f"خطأ في البحث: {e}") |
| return jsonify({'videos': []}), 500 |
|
|
| @app.route('/api/like/<int:video_id>', methods=['POST']) |
| @login_required |
| @limiter.limit("30 per minute") |
| 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=video_id, |
| priority=1) |
| |
| 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"خطأ في like_video: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/view/<int:video_id>', 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) |
| watch_percentage = data.get('watch_percentage', 0) |
| completed = data.get('completed', False) |
| ip = get_client_ip() |
| |
| 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, watch_percentage, completed) |
| VALUES (?, ?, 1, ?, ?, ?) |
| ON CONFLICT(user_id, video_id) DO UPDATE SET |
| watched = 1, |
| watch_time = watch_time + ?, |
| watch_percentage = MAX(watch_percentage, ?), |
| completed = MAX(completed, ?) |
| ''', (user_id, video_id, watch_time, watch_percentage, completed, |
| watch_time, watch_percentage, completed)) |
| |
| db.execute('UPDATE videos SET views = views + 1 WHERE id = ?', (video_id,)) |
| else: |
| db.execute(''' |
| UPDATE interactions SET |
| watch_time = watch_time + ?, |
| watch_percentage = MAX(watch_percentage, ?), |
| completed = MAX(completed, ?) |
| WHERE user_id = ? AND video_id = ? |
| ''', (watch_time, watch_percentage, completed, user_id, video_id)) |
| |
| |
| db.execute(''' |
| INSERT INTO video_views (user_id, video_id, watch_time, watch_percentage, completed, ip_address, user_agent) |
| VALUES (?, ?, ?, ?, ?, ?, ?) |
| ''', (user_id, video_id, watch_time, watch_percentage, completed, ip, |
| request.headers.get('User-Agent', ''))) |
| |
| |
| db.execute(''' |
| UPDATE videos SET |
| avg_watch_time = ( |
| SELECT AVG(watch_time) FROM video_views WHERE video_id = ? |
| ), |
| completion_rate = ( |
| SELECT AVG(watch_percentage) FROM video_views WHERE video_id = ? |
| ) |
| WHERE id = ? |
| ''', (video_id, video_id, video_id)) |
| |
| db.commit() |
| |
| |
| if completed: |
| add_xp(user_id, 'complete_view', 5) |
| elif watch_percentage > 50: |
| add_xp(user_id, 'partial_view', 2) |
| |
| track_event(user_id, 'view_video', { |
| 'video_id': video_id, |
| 'watch_time': watch_time, |
| 'watch_percentage': watch_percentage, |
| 'completed': completed |
| }) |
| |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في view_video: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/share/<int:video_id>', methods=['POST']) |
| @login_required |
| def share_video(video_id): |
| try: |
| user_id = session['user_id'] |
| platform = request.get_json().get('platform', 'internal') |
| |
| 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() |
| |
| shares = db.execute('SELECT shares_count FROM videos WHERE id = ?', |
| (video_id,)).fetchone()['shares_count'] |
| |
| |
| add_coins(user_id, 10, 'مكافأة مشاركة فيديو') |
| add_xp(user_id, 'share', 5) |
| track_event(user_id, 'share_video', {'video_id': video_id, 'platform': platform}) |
| |
| return jsonify({'status': 'ok', 'shares_count': shares}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في share_video: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/save/<int:video_id>', 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'] |
| |
| 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"خطأ في save_video: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/saved') |
| @login_required |
| def get_saved_videos(): |
| """الحصول على الفيديوهات المحفوظة""" |
| try: |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 20)) |
| offset = (page - 1) * limit |
| |
| db = get_db() |
| videos = db.execute(''' |
| SELECT v.*, u.username, u.avatar, u.is_verified |
| 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 |
| LIMIT ? OFFSET ? |
| ''', (session['user_id'], limit, offset)).fetchall() |
| |
| result = [] |
| for v in videos: |
| video = dict(v) |
| video['url'] = v['cloudinary_url'] if v['cloudinary_url'] else f'/videos/{v["filename"]}' |
| video['avatar_url'] = f'/avatars/{v["avatar"]}' |
| video.pop('vector', None) |
| result.append(video) |
| |
| return jsonify({ |
| 'videos': result, |
| 'next_page': page + 1 if len(result) == limit else None |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في get_saved_videos: {e}") |
| return jsonify([]), 500 |
|
|
| |
| @app.route('/api/comments/<int:video_id>') |
| @login_required |
| def get_comments(video_id): |
| try: |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 20)) |
| offset = (page - 1) * limit |
| |
| db = get_db() |
| |
| |
| comments = db.execute(''' |
| SELECT c.*, u.username, u.avatar, u.is_verified, u.role, |
| (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 ? OFFSET ? |
| ''', (video_id, limit, offset)).fetchall() |
| |
| result = [] |
| for comment in comments: |
| c = dict(comment) |
| |
| |
| replies = db.execute(''' |
| SELECT c.*, u.username, u.avatar, u.is_verified, u.role, |
| (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 5 |
| ''', (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) |
| |
| total = db.execute('SELECT COUNT(*) FROM comments WHERE video_id = ? AND parent_id IS NULL', |
| (video_id,)).fetchone()[0] |
| |
| return jsonify({ |
| 'comments': result, |
| 'total': total, |
| 'page': page, |
| 'has_more': len(result) == limit |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في get_comments: {e}") |
| return jsonify({'comments': [], 'error': str(e)}), 500 |
|
|
| @app.route('/api/comment/<int:video_id>', methods=['POST']) |
| @login_required |
| @limiter.limit("20 per minute") |
| 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) > 1000: |
| return jsonify({'error': 'التعليق طويل جداً'}), 400 |
| |
| |
| moderation = moderate_comment(comment_text) |
| |
| if not moderation['is_appropriate']: |
| if moderation['action'] == 'block': |
| return jsonify({'error': 'التعليق غير مناسب'}), 400 |
| elif moderation['action'] == 'flag': |
| |
| pass |
| |
| db = get_db() |
| cursor = db.execute(''' |
| INSERT INTO comments (user_id, video_id, parent_id, comment_text, is_moderated) |
| VALUES (?, ?, ?, ?, ?) |
| ''', (user_id, video_id, parent_id, comment_text, |
| not moderation['is_appropriate'])) |
| |
| 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() |
| add_notification(video['user_id'], user_id, 'comment', |
| f'علق {user["username"]} على فيديو لك', |
| video_id=video_id, comment_id=comment_id, |
| priority=1) |
| |
| 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() |
| add_notification(parent['user_id'], user_id, 'reply', |
| f'رد {user["username"]} على تعليقك', |
| video_id=video_id, comment_id=comment_id, |
| priority=1) |
| |
| 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"خطأ في add_comment: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/comment/like/<int:comment_id>', 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() |
| add_notification(comment['user_id'], user_id, 'like_comment', |
| f'أعجب {user["username"]} بتعليقك', |
| priority=0) |
| |
| 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"خطأ في like_comment: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/comment/pin/<int:comment_id>', methods=['POST']) |
| @login_required |
| def pin_comment(comment_id): |
| """تثبيت تعليق (لصاحب الفيديو فقط)""" |
| try: |
| user_id = session['user_id'] |
| db = get_db() |
| |
| comment = db.execute(''' |
| SELECT c.video_id, v.user_id |
| FROM comments c |
| JOIN videos v ON c.video_id = v.id |
| WHERE c.id = ? |
| ''', (comment_id,)).fetchone() |
| |
| if not comment or comment['user_id'] != user_id: |
| return jsonify({'error': 'غير مصرح'}), 403 |
| |
| db.execute('UPDATE comments SET is_pinned = 0 WHERE video_id = ?', (comment['video_id'],)) |
| db.execute('UPDATE comments SET is_pinned = 1 WHERE id = ?', (comment_id,)) |
| db.commit() |
| |
| track_event(user_id, 'pin_comment', {'comment_id': comment_id}) |
| |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في pin_comment: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/comment/delete/<int:comment_id>', methods=['POST']) |
| @login_required |
| def delete_comment(comment_id): |
| try: |
| user_id = session['user_id'] |
| db = get_db() |
| |
| comment = db.execute(''' |
| SELECT c.user_id, c.video_id, v.user_id as video_owner |
| FROM comments c |
| JOIN videos v ON c.video_id = v.id |
| WHERE c.id = ? |
| ''', (comment_id,)).fetchone() |
| |
| if not comment: |
| return jsonify({'error': 'التعليق غير موجود'}), 404 |
| |
| is_owner = comment['user_id'] == user_id |
| is_video_owner = comment['video_owner'] == user_id |
| is_admin = session.get('role') in ['admin', 'moderator'] |
| |
| if not (is_owner or is_video_owner or is_admin): |
| return jsonify({'error': 'غير مصرح'}), 403 |
| |
| db.execute('DELETE FROM comments WHERE id = ? OR parent_id = ?', (comment_id, comment_id)) |
| db.execute('UPDATE videos SET comments_count = comments_count - 1 WHERE id = ?', |
| (comment['video_id'],)) |
| db.commit() |
| |
| track_event(user_id, 'delete_comment', {'comment_id': comment_id}) |
| |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في delete_comment: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| |
| @app.route('/api/notifications/stream') |
| @login_required |
| def notification_stream(): |
| """بث الإشعارات المباشر باستخدام Server-Sent Events""" |
| 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"event: notification\ndata: {json.dumps(notification, ensure_ascii=False)}\n\n" |
| except queue.Empty: |
| yield "event: ping\ndata: {}\n\n" |
| 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', |
| 'Connection': 'keep-alive' |
| } |
| ) |
|
|
| @app.route('/api/notifications') |
| @login_required |
| def get_notifications(): |
| """الحصول على الإشعارات مع ترقيم الصفحات""" |
| try: |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 20)) |
| offset = (page - 1) * limit |
| |
| 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.priority DESC, n.created_at DESC |
| LIMIT ? OFFSET ? |
| ''', (session['user_id'], limit, offset)).fetchall() |
| |
| |
| if page == 1: |
| 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) |
| |
| unread_count = db.execute(''' |
| SELECT COUNT(*) FROM notifications |
| WHERE user_id = ? AND is_read = 0 |
| ''', (session['user_id'],)).fetchone()[0] |
| |
| return jsonify({ |
| 'notifications': result, |
| 'unread_count': unread_count, |
| 'next_page': page + 1 if len(result) == limit else None |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في get_notifications: {e}") |
| return jsonify({'notifications': [], 'unread_count': 0}), 500 |
|
|
| @app.route('/api/notifications/count') |
| @login_required |
| def notification_count(): |
| """عدد الإشعارات غير المقروءة""" |
| 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}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في notification_count: {e}") |
| return jsonify({'count': 0}), 500 |
|
|
| @app.route('/api/notifications/mark-read', methods=['POST']) |
| @login_required |
| def mark_notifications_read(): |
| """تحديد الإشعارات كمقروءة""" |
| try: |
| notification_id = request.get_json().get('notification_id') |
| |
| db = get_db() |
| if notification_id: |
| db.execute('UPDATE notifications SET is_read = 1 WHERE id = ? AND user_id = ?', |
| (notification_id, session['user_id'])) |
| else: |
| db.execute('UPDATE notifications SET is_read = 1 WHERE user_id = ?', |
| (session['user_id'],)) |
| |
| db.commit() |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في mark_notifications_read: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| |
| @app.route('/live') |
| @login_required |
| def live_page(): |
| """صفحة البث المباشر""" |
| return render_template_string(LIVE_TEMPLATE, session=session) |
|
|
| @app.route('/api/live/start', methods=['POST']) |
| @login_required |
| def start_live(): |
| """بدء بث مباشر (يتطلب عضوية VIP)""" |
| try: |
| |
| benefits = get_membership_benefits(session['user_id']) |
| if not benefits.can_live_stream: |
| return jsonify({'error': 'البث المباشر متاح فقط لأعضاء VIP'}), 403 |
| |
| data = request.get_json() |
| title = data.get('title', 'بث مباشر جديد') |
| description = data.get('description', '') |
| |
| db = get_db() |
| stream_key = hashlib.sha256(f"{session['user_id']}_{uuid.uuid4()}_{time.time()}".encode()).hexdigest() |
| |
| |
| db.execute('UPDATE live_streams SET is_active = 0, ended_at = CURRENT_TIMESTAMP WHERE user_id = ? AND is_active = 1', |
| (session['user_id'],)) |
| |
| cursor = db.execute(''' |
| INSERT INTO live_streams (user_id, stream_key, title, description) |
| VALUES (?, ?, ?, ?) |
| ''', (session['user_id'], stream_key, title, description)) |
| |
| stream_id = cursor.lastrowid |
| |
| db.execute('UPDATE users SET is_live = 1 WHERE id = ?', (session['user_id'],)) |
| db.commit() |
| |
| |
| active_streams[stream_id] = { |
| 'user_id': session['user_id'], |
| 'viewers': 0, |
| 'started_at': datetime.now() |
| } |
| |
| |
| followers = db.execute('SELECT follower_id FROM follows WHERE user_id = ?', |
| (session['user_id'],)).fetchall() |
| follower_ids = [f['follower_id'] for f in followers] |
| |
| send_bulk_notifications( |
| follower_ids, |
| session['user_id'], |
| 'live_start', |
| f'{session["username"]} بدأ بثاً مباشراً!', |
| action_url=f'/live/{stream_id}', |
| priority=2 |
| ) |
| |
| track_event(session['user_id'], 'start_live', {'stream_id': stream_id, 'title': title}) |
| |
| return jsonify({ |
| 'status': 'ok', |
| 'stream_id': stream_id, |
| 'stream_key': stream_key, |
| 'rtmp_url': 'rtmp://your-server.com/live' |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في start_live: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/live/stop', methods=['POST']) |
| @login_required |
| def stop_live(): |
| """إنهاء بث مباشر""" |
| try: |
| db = get_db() |
| |
| stream = db.execute('SELECT id FROM live_streams WHERE user_id = ? AND is_active = 1', |
| (session['user_id'],)).fetchone() |
| |
| if stream: |
| db.execute('UPDATE live_streams SET is_active = 0, ended_at = CURRENT_TIMESTAMP WHERE id = ?', |
| (stream['id'],)) |
| |
| if stream['id'] in active_streams: |
| del active_streams[stream['id']] |
| |
| db.execute('UPDATE users SET is_live = 0 WHERE id = ?', (session['user_id'],)) |
| db.commit() |
| |
| track_event(session['user_id'], 'stop_live', {}) |
| |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في stop_live: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/live/active') |
| @login_required |
| def get_active_streams(): |
| """الحصول على البثوث النشطة""" |
| try: |
| db = get_db() |
| |
| streams = db.execute(''' |
| SELECT ls.*, u.username, u.avatar, u.is_verified |
| FROM live_streams ls |
| JOIN users u ON ls.user_id = u.id |
| WHERE ls.is_active = 1 |
| ORDER BY ls.viewers DESC |
| LIMIT 50 |
| ''').fetchall() |
| |
| result = [] |
| for s in streams: |
| stream = dict(s) |
| stream['avatar_url'] = f'/avatars/{s["avatar"]}' |
| result.append(stream) |
| |
| return jsonify(result) |
| |
| except Exception as e: |
| logger.error(f"خطأ في get_active_streams: {e}") |
| return jsonify([]), 500 |
|
|
| @app.route('/api/live/<int:stream_id>/view', methods=['POST']) |
| @login_required |
| def view_stream(stream_id): |
| """تسجيل مشاهدة بث""" |
| try: |
| db = get_db() |
| db.execute('UPDATE live_streams SET viewers = viewers + 1, peak_viewers = MAX(peak_viewers, viewers + 1) WHERE id = ?', |
| (stream_id,)) |
| db.commit() |
| |
| if stream_id in active_streams: |
| active_streams[stream_id]['viewers'] += 1 |
| |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في view_stream: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/api/live/gift/<int:stream_id>', methods=['POST']) |
| @login_required |
| def send_gift(stream_id): |
| """إرسال هدية في البث""" |
| try: |
| data = request.get_json() |
| gift_id = data.get('gift_id') |
| |
| db = get_db() |
| |
| |
| gift = db.execute('SELECT * FROM virtual_gifts WHERE id = ?', (gift_id,)).fetchone() |
| if not gift: |
| return jsonify({'error': 'الهدية غير موجودة'}), 404 |
| |
| price = gift['price'] |
| |
| |
| user = db.execute('SELECT coins FROM users WHERE id = ?', (session['user_id'],)).fetchone() |
| if user['coins'] < price: |
| return jsonify({'error': 'رصيد غير كافٍ'}), 400 |
| |
| |
| if deduct_coins(session['user_id'], price, f'إرسال هدية {gift["name"]}'): |
| |
| cursor = db.execute(''' |
| INSERT INTO live_gifts (stream_id, user_id, gift_type, gift_value) |
| VALUES (?, ?, ?, ?) |
| ''', (stream_id, session['user_id'], gift['name'], price)) |
| |
| gift_record_id = cursor.lastrowid |
| |
| |
| stream = db.execute('SELECT user_id FROM live_streams WHERE id = ?', (stream_id,)).fetchone() |
| if stream: |
| stream_owner = stream['user_id'] |
| owner_share = price // 2 |
| add_coins(stream_owner, owner_share, f'هدية {gift["name"]} من {session["username"]}') |
| |
| |
| db.execute('UPDATE live_streams SET gifts_value = gifts_value + ? WHERE id = ?', |
| (price, stream_id)) |
| |
| |
| add_notification(stream_owner, session['user_id'], 'gift', |
| f'أرسل {session["username"]} هدية {gift["name"]} في بثك', |
| gift_id=gift_record_id, |
| priority=2, |
| image_url=gift['image_url']) |
| |
| db.commit() |
| |
| track_event(session['user_id'], 'send_gift', |
| {'stream_id': stream_id, 'gift_id': gift_id, 'price': price}) |
| |
| return jsonify({'status': 'ok'}) |
| else: |
| return jsonify({'error': 'فشل في خصم العملات'}), 400 |
| |
| except Exception as e: |
| logger.error(f"خطأ في send_gift: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| |
| @app.route('/api/gamification/xp') |
| @login_required |
| def get_xp(): |
| """الحصول على نقاط الخبرة والتاريخ""" |
| db = get_db() |
| user = db.execute('SELECT xp, level FROM users WHERE id = ?', (session['user_id'],)).fetchone() |
| |
| |
| history = db.execute(''' |
| SELECT action, xp_gained, created_at FROM user_xp |
| WHERE user_id = ? ORDER BY created_at DESC LIMIT 50 |
| ''', (session['user_id'],)).fetchall() |
| |
| |
| current_level_xp = calculate_xp_for_level(user['level']) |
| next_level_xp = calculate_xp_for_level(user['level'] + 1) |
| progress = ((user['xp'] - current_level_xp) / (next_level_xp - current_level_xp)) * 100 if next_level_xp > current_level_xp else 100 |
| |
| return jsonify({ |
| 'xp': user['xp'], |
| 'level': user['level'], |
| 'next_level_xp': next_level_xp, |
| 'progress': min(progress, 100), |
| 'history': [dict(h) for h in history] |
| }) |
|
|
| @app.route('/api/gamification/daily', methods=['POST']) |
| @login_required |
| def claim_daily(): |
| """المطالبة بالمكافأة اليومية""" |
| success, *rest = claim_daily_reward(session['user_id']) |
| |
| if success: |
| coins, xp, streak = rest |
| track_event(session['user_id'], 'claim_daily', {'coins': coins, 'xp': xp, 'streak': streak}) |
| return jsonify({'status': 'ok', 'coins': coins, 'xp': xp, 'streak': streak}) |
| else: |
| return jsonify({'error': rest[0]}), 400 |
|
|
| @app.route('/api/gamification/leaderboard') |
| @login_required |
| def leaderboard(): |
| """لوحة المتصدرين""" |
| period = request.args.get('period', 'all') |
| limit = int(request.args.get('limit', 50)) |
| |
| db = get_db() |
| |
| if period == 'daily': |
| |
| users = db.execute(''' |
| SELECT u.username, u.avatar, u.is_verified, SUM(x.xp_gained) as daily_xp, u.level |
| FROM user_xp x |
| JOIN users u ON x.user_id = u.id |
| WHERE DATE(x.created_at) = DATE('now') |
| GROUP BY x.user_id |
| ORDER BY daily_xp DESC |
| LIMIT ? |
| ''', (limit,)).fetchall() |
| elif period == 'weekly': |
| |
| users = db.execute(''' |
| SELECT u.username, u.avatar, u.is_verified, SUM(x.xp_gained) as weekly_xp, u.level |
| FROM user_xp x |
| JOIN users u ON x.user_id = u.id |
| WHERE x.created_at > datetime('now', '-7 days') |
| GROUP BY x.user_id |
| ORDER BY weekly_xp DESC |
| LIMIT ? |
| ''', (limit,)).fetchall() |
| else: |
| |
| 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) |
| |
| |
| if period == 'all': |
| rank = db.execute(''' |
| SELECT COUNT(*) + 1 as rank |
| FROM users |
| WHERE xp > (SELECT xp FROM users WHERE id = ?) |
| ''', (session['user_id'],)).fetchone()[0] |
| else: |
| rank = None |
| |
| return jsonify({ |
| 'leaderboard': result, |
| 'user_rank': rank |
| }) |
|
|
| @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"خطأ في coins_balance: {e}") |
| return jsonify({'coins': 0, 'diamonds': 0}), 500 |
|
|
| @app.route('/api/transactions') |
| @login_required |
| def get_transactions(): |
| """سجل المعاملات""" |
| try: |
| page = int(request.args.get('page', 1)) |
| limit = int(request.args.get('limit', 20)) |
| offset = (page - 1) * limit |
| |
| db = get_db() |
| transactions = db.execute(''' |
| SELECT * FROM transactions |
| WHERE user_id = ? |
| ORDER BY created_at DESC |
| LIMIT ? OFFSET ? |
| ''', (session['user_id'], limit, offset)).fetchall() |
| |
| return jsonify({ |
| 'transactions': [dict(t) for t in transactions], |
| 'next_page': page + 1 if len(transactions) == limit else None |
| }) |
| |
| except Exception as e: |
| logger.error(f"خطأ في get_transactions: {e}") |
| return jsonify({'transactions': []}), 500 |
|
|
| @app.route('/api/gifts') |
| @login_required |
| def get_gifts(): |
| """الحصول على قائمة الهدايا المتاحة""" |
| try: |
| db = get_db() |
| gifts = db.execute('SELECT * FROM virtual_gifts WHERE is_active = 1 ORDER BY price').fetchall() |
| return jsonify([dict(g) for g in gifts]) |
| |
| except Exception as e: |
| logger.error(f"خطأ في get_gifts: {e}") |
| return jsonify([]), 500 |
|
|
| |
| def track_event(user_id, event_name, event_data=None): |
| """تسجيل حدث تحليلي""" |
| db = get_db() |
| try: |
| db.execute(''' |
| INSERT INTO analytics_events (user_id, session_id, event_name, event_data, ip_address, user_agent, page_url) |
| VALUES (?, ?, ?, ?, ?, ?, ?) |
| ''', (user_id, session.get('session_id'), event_name, |
| json.dumps(event_data) if event_data else None, |
| get_client_ip(), |
| request.headers.get('User-Agent', ''), |
| request.path)) |
| db.commit() |
| except Exception as e: |
| logger.error(f"خطأ في track_event: {e}") |
|
|
| @app.route('/api/analytics/stats') |
| @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'}) |
|
|
| |
| def generate_affiliate_link(user_id): |
| """توليد رابط إحالة فريد""" |
| code = secrets.token_urlsafe(10) |
| db = get_db() |
| db.execute('UPDATE users SET referral_code = ? WHERE id = ?', (code, 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() |
| |
| |
| log_file = os.path.join('affiliate_logs', f"clicks_{datetime.now().strftime('%Y%m')}.log") |
| with open(log_file, 'a', encoding='utf-8') as f: |
| f.write(f"{datetime.now().isoformat()}|{referrer['id']}|{ip}|{user_agent}\n") |
| |
| return True |
| |
| except Exception as e: |
| logger.error(f"خطأ في track_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 = 200 |
| coins_referred = 100 |
| xp_referrer = 50 |
| |
| add_coins(referrer['id'], coins_referrer, 'مكافأة إحالة') |
| add_coins(new_user_id, coins_referred, 'مكافأة تسجيل عن طريق رابط') |
| add_xp(referrer['id'], 'referral', xp_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_logs', f"conversions_{datetime.now().strftime('%Y%m')}.log") |
| with open(log_file, 'a', encoding='utf-8') as f: |
| f.write(f"{datetime.now().isoformat()}|{referrer['id']}|{new_user_id}\n") |
| |
| except Exception as e: |
| logger.error(f"خطأ في process_affiliate_conversion: {e}") |
|
|
| @app.route('/api/affiliate/link') |
| @login_required |
| def get_affiliate_link(): |
| """الحصول على رابط الإحالة""" |
| link = generate_affiliate_link(session['user_id']) |
| return jsonify({'link': request.host_url.rstrip('/') + link}) |
|
|
| @app.route('/api/affiliate/stats') |
| @login_required |
| def affiliate_stats(): |
| """إحصائيات الإحالات""" |
| 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() |
| |
| return jsonify({ |
| 'clicks': user['affiliate_clicks'], |
| 'conversions': user['affiliate_conversions'], |
| 'balance': user['affiliate_balance'], |
| 'recent_clicks': [dict(c) for c in clicks] |
| }) |
|
|
| @app.route('/r/<referral_code>') |
| def referral_redirect(referral_code): |
| """تسجيل نقرة وإعادة توجيه للتسجيل""" |
| ip = get_client_ip() |
| 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/report', methods=['POST']) |
| @login_required |
| def create_report(): |
| """إنشاء بلاغ""" |
| try: |
| data = request.get_json() |
| report_type = data.get('type') |
| reported_id = data.get('id') |
| reason = data.get('reason') |
| details = data.get('details', '') |
| |
| if not reason: |
| return jsonify({'error': 'يرجى ذكر سبب البلاغ'}), 400 |
| |
| if len(reason) < 10: |
| return jsonify({'error': 'الرجاء كتابة سبب أكثر تفصيلاً'}), 400 |
| |
| db = get_db() |
| |
| if report_type == 'user': |
| db.execute(''' |
| INSERT INTO reports (reporter_id, reported_user_id, reason, details) |
| VALUES (?, ?, ?, ?) |
| ''', (session['user_id'], reported_id, reason, details)) |
| |
| elif report_type == 'video': |
| db.execute(''' |
| INSERT INTO reports (reporter_id, reported_video_id, reason, details) |
| VALUES (?, ?, ?, ?) |
| ''', (session['user_id'], reported_id, reason, details)) |
| |
| db.execute('UPDATE videos SET report_count = report_count + 1 WHERE id = ?', (reported_id,)) |
| |
| video = db.execute('SELECT report_count FROM videos WHERE id = ?', (reported_id,)).fetchone() |
| if video and video['report_count'] >= 5: |
| db.execute('UPDATE videos SET is_reported = 1 WHERE id = ?', (reported_id,)) |
| |
| elif report_type == 'comment': |
| db.execute(''' |
| INSERT INTO reports (reporter_id, reported_comment_id, reason, details) |
| VALUES (?, ?, ?, ?) |
| ''', (session['user_id'], reported_id, reason, details)) |
| |
| db.execute('UPDATE comments SET report_count = report_count + 1 WHERE id = ?', (reported_id,)) |
| |
| comment = db.execute('SELECT report_count FROM comments WHERE id = ?', (reported_id,)).fetchone() |
| if comment and comment['report_count'] >= 3: |
| db.execute('UPDATE comments SET is_reported = 1 WHERE id = ?', (reported_id,)) |
| |
| else: |
| return jsonify({'error': 'نوع البلاغ غير صحيح'}), 400 |
| |
| db.commit() |
| |
| |
| add_coins(session['user_id'], 5, 'مكافأة الإبلاغ عن محتوى غير لائق') |
| track_event(session['user_id'], 'create_report', {'type': report_type, 'id': reported_id}) |
| |
| return jsonify({'status': 'ok'}) |
| |
| except Exception as e: |
| logger.error(f"خطأ في create_report: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| |
| @app.route('/videos/<path:filename>') |
| def serve_video(filename): |
| """تقديم ملفات الفيديو""" |
| return send_from_directory(app.config['UPLOAD_FOLDER'], filename) |
|
|
| @app.route('/avatars/<path:filename>') |
| def serve_avatar(filename): |
| """تقديم الصور الشخصية""" |
| return send_from_directory(app.config['AVATAR_FOLDER'], filename) |
|
|
| @app.route('/thumbnails/<path:filename>') |
| def serve_thumbnail(filename): |
| """تقديم الصور المصغرة""" |
| return send_from_directory(app.config['THUMBNAIL_FOLDER'], filename) |
|
|
| @app.route('/encrypted/<path:filename>') |
| @login_required |
| def serve_encrypted(filename): |
| """تقديم الفيديوهات المشفرة""" |
| return send_from_directory(app.config['ENCRYPTED_FOLDER'], filename) |
|
|
| @app.route('/watermarked/<path:filename>') |
| def serve_watermarked(filename): |
| """تقديم الفيديوهات ذات العلامة المائية""" |
| return send_from_directory(app.config['WATERMARK_FOLDER'], filename) |
|
|
| |
| @app.route('/search') |
| @login_required |
| def search_page(): |
| """صفحة البحث""" |
| return render_template_string(SEARCH_TEMPLATE, session=session) |
|
|
| @app.route('/leaderboard') |
| @login_required |
| def leaderboard_page(): |
| """صفحة المتصدرين""" |
| return render_template_string(LEADERBOARD_TEMPLATE, session=session) |
|
|
| @app.route('/gifts') |
| @login_required |
| def gifts_page(): |
| """صفحة الهدايا""" |
| return render_template_string(GIFTS_TEMPLATE, session=session) |
|
|
| |
| LOGIN_PAGE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>تسجيل الدخول - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass { |
| background: rgba(255, 255, 255, 0.05); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .gradient-text { |
| background: linear-gradient(135deg, #ff2d55, #ff8a5c); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| transition: all 0.3s ease; |
| } |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 25px -5px rgba(255, 45, 85, 0.5); |
| } |
| .input-glass { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| transition: all 0.3s ease; |
| } |
| .input-glass:focus { |
| border-color: #ff2d55; |
| box-shadow: 0 0 0 2px rgba(255, 45, 85, 0.2); |
| } |
| </style> |
| </head> |
| <body class="min-h-screen bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] flex items-center justify-center p-4"> |
| <div class="absolute inset-0 overflow-hidden"> |
| <div class="absolute top-0 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-blob"></div> |
| <div class="absolute top-0 -right-4 w-72 h-72 bg-yellow-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-blob animation-delay-2000"></div> |
| <div class="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-blob animation-delay-4000"></div> |
| </div> |
| |
| <div class="glass-card rounded-3xl p-8 w-full max-w-md relative z-10"> |
| <div class="text-center mb-8"> |
| <h1 class="text-6xl font-black gradient-text">ARC</h1> |
| <p class="text-gray-400 mt-2">منصة الفيديو الأكثر تطوراً</p> |
| </div> |
| |
| <form method="post" class="space-y-6"> |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">اسم المستخدم أو البريد</label> |
| <input type="text" name="username" required |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">كلمة المرور</label> |
| <input type="password" name="password" required |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| </div> |
| |
| <div class="flex items-center justify-between"> |
| <label class="flex items-center text-gray-300"> |
| <input type="checkbox" name="remember" class="form-checkbox text-[#ff2d55] rounded bg-transparent border-gray-600"> |
| <span class="mr-2 text-sm">تذكرني</span> |
| </label> |
| <a href="/forgot-password" class="text-sm text-[#ff2d55] hover:text-[#ff4d6d] transition">نسيت كلمة المرور؟</a> |
| </div> |
| |
| <button type="submit" class="w-full btn-primary text-white font-bold py-3 px-4 rounded-2xl text-lg"> |
| دخول |
| </button> |
| </form> |
| |
| {% if error %} |
| <div class="mt-4 p-3 bg-red-500/20 border border-red-500/50 rounded-2xl text-red-400 text-center"> |
| {{ error }} |
| </div> |
| {% endif %} |
| |
| <div class="mt-6 text-center"> |
| <p class="text-gray-400">ليس لديك حساب؟ |
| <a href="/register" class="text-[#ff2d55] hover:text-[#ff4d6d] font-bold transition">سجل الآن</a> |
| </p> |
| </div> |
| |
| <div class="mt-8 pt-6 border-t border-gray-800 text-center"> |
| <p class="text-xs text-gray-500"> |
| بالتسجيل أنت توافق على |
| <a href="#" class="text-gray-400 hover:text-[#ff2d55]">الشروط والأحكام</a> |
| و |
| <a href="#" class="text-gray-400 hover:text-[#ff2d55]">سياسة الخصوصية</a> |
| </p> |
| </div> |
| </div> |
| </body> |
| </html> |
| ''' |
|
|
| REGISTER_PAGE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>تسجيل جديد - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .gradient-text { |
| background: linear-gradient(135deg, #ff2d55, #ff8a5c); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| transition: all 0.3s ease; |
| } |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 25px -5px rgba(255, 45, 85, 0.5); |
| } |
| .input-glass { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| transition: all 0.3s ease; |
| } |
| .input-glass:focus { |
| border-color: #ff2d55; |
| box-shadow: 0 0 0 2px rgba(255, 45, 85, 0.2); |
| } |
| </style> |
| </head> |
| <body class="min-h-screen bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] flex items-center justify-center p-4"> |
| <div class="glass-card rounded-3xl p-8 w-full max-w-md"> |
| <div class="text-center mb-8"> |
| <h1 class="text-5xl font-black gradient-text">انضم إلينا</h1> |
| <p class="text-gray-400 mt-2">أنشئ حسابك وابدأ الإبداع</p> |
| </div> |
| |
| <form method="post" class="space-y-4"> |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">اسم المستخدم</label> |
| <input type="text" name="username" required |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">البريد الإلكتروني</label> |
| <input type="email" name="email" required |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">كلمة المرور</label> |
| <input type="password" name="password" required |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| <p class="text-xs text-gray-500 mt-1">8 أحرف على الأقل، حرف كبير وصغير ورقم</p> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">تأكيد كلمة المرور</label> |
| <input type="password" name="confirm_password" required |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2 text-sm">كود الإحالة (اختياري)</label> |
| <input type="text" name="referral_code" value="{{ request.args.get('ref', '') }}" |
| class="w-full px-4 py-3 input-glass rounded-2xl text-white focus:outline-none"> |
| </div> |
| |
| <label class="flex items-center text-gray-300"> |
| <input type="checkbox" name="agree_terms" required class="form-checkbox text-[#ff2d55] rounded bg-transparent border-gray-600"> |
| <span class="mr-2 text-sm">أوافق على <a href="#" class="text-[#ff2d55]">الشروط والأحكام</a></span> |
| </label> |
| |
| <button type="submit" class="w-full btn-primary text-white font-bold py-3 px-4 rounded-2xl text-lg mt-4"> |
| تسجيل |
| </button> |
| </form> |
| |
| {% if error %} |
| <div class="mt-4 p-3 bg-red-500/20 border border-red-500/50 rounded-2xl text-red-400 text-center"> |
| {{ error }} |
| </div> |
| {% endif %} |
| |
| <div class="mt-6 text-center"> |
| <p class="text-gray-400">لديك حساب بالفعل؟ |
| <a href="/login" class="text-[#ff2d55] hover:text-[#ff4d6d] font-bold transition">سجل دخول</a> |
| </p> |
| </div> |
| </div> |
| </body> |
| </html> |
| ''' |
|
|
| TWOFA_PAGE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>المصادقة الثنائية - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| } |
| .input-glass { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| </style> |
| </head> |
| <body class="min-h-screen bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] flex items-center justify-center p-4"> |
| <div class="glass-card rounded-3xl p-8 w-full max-w-md"> |
| <div class="text-center mb-8"> |
| <div class="text-5xl mb-4">🔐</div> |
| <h2 class="text-2xl font-bold text-white">المصادقة الثنائية</h2> |
| <p class="text-gray-400 mt-2">أدخل الرمز من تطبيق المصادقة</p> |
| </div> |
| |
| <form method="post" action="/login" class="space-y-6"> |
| <input type="hidden" name="username" value="{{ request.form.username }}"> |
| <input type="hidden" name="password" value="{{ request.form.password }}"> |
| |
| <div> |
| <input type="text" name="twofa_code" required |
| class="w-full px-4 py-4 input-glass rounded-2xl text-white text-center text-2xl tracking-widest focus:outline-none focus:border-[#ff2d55]" |
| placeholder="000000" maxlength="6"> |
| </div> |
| |
| <button type="submit" class="w-full btn-primary text-white font-bold py-3 px-4 rounded-2xl"> |
| تحقق |
| </button> |
| </form> |
| |
| <div class="mt-6 text-center"> |
| <a href="/login" class="text-gray-400 hover:text-[#ff2d55] transition">العودة لتسجيل الدخول</a> |
| </div> |
| </div> |
| </body> |
| </html> |
| ''' |
|
|
| SETUP_2FA_PAGE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>إعداد المصادقة الثنائية - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| } |
| </style> |
| </head> |
| <body class="min-h-screen bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] p-4"> |
| <div class="max-w-2xl mx-auto"> |
| <div class="glass-card rounded-3xl p-8"> |
| <h1 class="text-3xl font-bold text-white mb-6 text-center">إعداد المصادقة الثنائية</h1> |
| |
| <div class="space-y-8"> |
| <div class="text-center"> |
| <div class="text-6xl mb-4">📱</div> |
| <h2 class="text-xl font-bold text-white mb-2">1. ثبّت تطبيق مصادقة</h2> |
| <p class="text-gray-400">حمّل Google Authenticator أو Microsoft Authenticator</p> |
| </div> |
| |
| <div class="text-center"> |
| <h2 class="text-xl font-bold text-white mb-4">2. امسح رمز QR</h2> |
| {% if qr_code %} |
| <img src="data:image/png;base64,{{ qr_code }}" class="mx-auto bg-white p-4 rounded-2xl"> |
| {% endif %} |
| </div> |
| |
| <div class="text-center"> |
| <h2 class="text-xl font-bold text-white mb-2">أو أدخل المفتاح يدوياً</h2> |
| <div class="bg-gray-800 p-3 rounded-xl"> |
| <code class="text-[#ff2d55]">{{ secret }}</code> |
| </div> |
| </div> |
| |
| <div> |
| <h2 class="text-xl font-bold text-white mb-4">3. رموز الاسترداد الاحتياطية</h2> |
| <div class="bg-gray-800 p-4 rounded-xl grid grid-cols-2 gap-2"> |
| {% for code in backup_codes %} |
| <div class="text-center font-mono text-[#ff2d55]">{{ code }}</div> |
| {% endfor %} |
| </div> |
| <p class="text-sm text-gray-400 mt-2">احفظ هذه الرموز في مكان آمن، يمكنك استخدامها مرة واحدة إذا فقدت هاتفك</p> |
| </div> |
| |
| <div class="text-center"> |
| <h2 class="text-xl font-bold text-white mb-4">4. تحقق من الإعداد</h2> |
| <form method="post" class="max-w-xs mx-auto space-y-4"> |
| <input type="text" name="code" placeholder="أدخل الرمز" |
| class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-2xl text-white text-center" |
| maxlength="6"> |
| <button type="submit" class="w-full btn-primary text-white font-bold py-3 px-4 rounded-2xl"> |
| تحقق وفعّل |
| </button> |
| </form> |
| {% if error %} |
| <p class="text-red-400 mt-2">{{ error }}</p> |
| {% endif %} |
| </div> |
| </div> |
| </div> |
| </div> |
| </body> |
| </html> |
| ''' |
|
|
| MAIN_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>ARC Video - الصفحة الرئيسية</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| |
| .glass-sidebar { |
| background: rgba(20, 20, 30, 0.8); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border-left: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| |
| .glass-nav { |
| background: rgba(10, 10, 15, 0.9); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| border-top: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| |
| .fab { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| box-shadow: 0 10px 25px -5px rgba(255, 45, 85, 0.5); |
| transition: all 0.3s ease; |
| } |
| |
| .fab:hover { |
| transform: scale(1.1) translateY(-5px); |
| } |
| |
| .video-container { |
| scroll-snap-type: y mandatory; |
| height: calc(100vh - 80px); |
| overflow-y: scroll; |
| scroll-behavior: smooth; |
| } |
| |
| .video-item { |
| scroll-snap-align: start; |
| height: 100vh; |
| position: relative; |
| } |
| |
| .gradient-text { |
| background: linear-gradient(135deg, #ff2d55, #ff8a5c); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .action-btn { |
| background: rgba(0, 0, 0, 0.6); |
| backdrop-filter: blur(10px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| transition: all 0.3s ease; |
| } |
| |
| .action-btn:hover, .action-btn.active { |
| background: #ff2d55; |
| transform: scale(1.1); |
| } |
| |
| .comments-panel { |
| background: rgba(20, 20, 30, 0.95); |
| backdrop-filter: blur(20px); |
| border-top: 1px solid rgba(255, 45, 85, 0.3); |
| } |
| |
| @keyframes slideUp { |
| from { transform: translateY(100%); } |
| to { transform: translateY(0); } |
| } |
| |
| .slide-up { |
| animation: slideUp 0.3s ease-out; |
| } |
| |
| .notification-badge { |
| position: absolute; |
| top: -5px; |
| right: -5px; |
| background: #ff2d55; |
| color: white; |
| border-radius: 999px; |
| min-width: 18px; |
| height: 18px; |
| font-size: 10px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| border: 2px solid #1a1a1a; |
| } |
| |
| .vip-badge { |
| background: linear-gradient(135deg, #FFD700, #FFA500); |
| color: #000; |
| font-weight: bold; |
| } |
| |
| .gold-badge { |
| background: linear-gradient(135deg, #FFD700, #B8860B); |
| color: #000; |
| } |
| </style> |
| </head> |
| <body class="bg-black text-white h-screen overflow-hidden"> |
| <!-- الشريط الجانبي الأيمن (Glassmorphism) --> |
| <div class="fixed right-0 top-0 h-full w-64 glass-sidebar hidden lg:block p-6 z-30"> |
| <div class="flex items-center gap-3 mb-8"> |
| <div class="w-10 h-10 rounded-2xl bg-gradient-to-br from-[#ff2d55] to-[#ff8a5c] flex items-center justify-center"> |
| <span class="text-2xl font-black">A</span> |
| </div> |
| <span class="text-2xl font-black gradient-text">ARC</span> |
| </div> |
| |
| <nav class="space-y-2"> |
| <a href="#" onclick="loadFeed('for-you'); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl bg-[#ff2d55] text-white nav-item active"> |
| <span class="text-xl">🏠</span> |
| <span>الرئيسية</span> |
| </a> |
| <a href="#" onclick="loadFeed('friends'); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl hover:bg-white/5 transition nav-item"> |
| <span class="text-xl">👥</span> |
| <span>الأصدقاء</span> |
| </a> |
| <a href="#" onclick="loadFeed('trending'); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl hover:bg-white/5 transition nav-item"> |
| <span class="text-xl">🔥</span> |
| <span>رائج</span> |
| <span class="mr-auto text-xs bg-[#ff2d55] px-2 py-1 rounded-full">جديد</span> |
| </a> |
| <a href="#" onclick="openGlass('/glass/search'); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl hover:bg-white/5 transition"> |
| <span class="text-xl">🔍</span> |
| <span>بحث</span> |
| </a> |
| <a href="#" onclick="openGlass('/glass/saved'); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl hover:bg-white/5 transition"> |
| <span class="text-xl">📁</span> |
| <span>محفوظة</span> |
| </a> |
| <a href="#" onclick="openNotifications(); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl hover:bg-white/5 transition relative"> |
| <span class="text-xl">🔔</span> |
| <span>إشعارات</span> |
| <span id="sidebarNotifBadge" class="notification-badge hidden">0</span> |
| </a> |
| <a href="#" onclick="openGlass('/glass/leaderboard'); return false;" class="flex items-center gap-3 px-4 py-3 rounded-2xl hover:bg-white/5 transition"> |
| <span class="text-xl">🏆</span> |
| <span>المتصدرين</span> |
| </a> |
| </nav> |
| |
| <div class="absolute bottom-6 right-6 left-6"> |
| <div class="bg-white/5 rounded-2xl p-4"> |
| <div class="flex items-center gap-3"> |
| <div class="relative"> |
| <img src="/avatars/{{ session.avatar or 'default.jpg' }}" class="w-12 h-12 rounded-2xl object-cover border-2 border-[#ff2d55]"> |
| <div class="absolute -bottom-1 -left-1 w-4 h-4 bg-green-500 rounded-full border-2 border-black"></div> |
| </div> |
| <div class="flex-1"> |
| <div class="font-bold">@{{ session.username }}</div> |
| <div class="text-xs text-gray-400 flex items-center gap-1"> |
| <span>المستوى {{ session.level or 1 }}</span> |
| <span class="w-1 h-1 bg-gray-600 rounded-full"></span> |
| <span>XP: {{ session.xp or 0 }}</span> |
| </div> |
| </div> |
| <a href="/settings" class="text-2xl text-gray-400 hover:text-[#ff2d55] transition">⚙️</a> |
| </div> |
| <div class="mt-3 flex justify-between text-xs"> |
| <a href="/profile/{{ session.user_id }}" class="text-center"> |
| <div class="font-bold">{{ session.followers_count or 0 }}</div> |
| <div class="text-gray-500">متابع</div> |
| </a> |
| <a href="/profile/{{ session.user_id }}?tab=following" class="text-center"> |
| <div class="font-bold">{{ session.following_count or 0 }}</div> |
| <div class="text-gray-500">يتابع</div> |
| </a> |
| <a href="/profile/{{ session.user_id }}" class="text-center"> |
| <div class="font-bold">{{ session.videos_count or 0 }}</div> |
| <div class="text-gray-500">فيديو</div> |
| </a> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- المحتوى الرئيسي --> |
| <div class="lg:mr-64 h-screen"> |
| <div id="videoContainer" class="video-container"></div> |
| </div> |
| |
| <!-- Floating Action Button (تصوير) --> |
| <a href="/upload" class="fab fixed left-6 bottom-24 lg:bottom-6 w-14 h-14 rounded-full flex items-center justify-center text-2xl z-40"> |
| 📸 |
| </a> |
| |
| <!-- الشريط السفلي للموبايل --> |
| <div class="lg:hidden glass-nav fixed bottom-0 left-0 right-0 h-20 flex items-center justify-around px-4 z-30"> |
| <a href="#" onclick="loadFeed('for-you'); return false;" class="flex flex-col items-center text-gray-400 hover:text-[#ff2d55] transition nav-item-mobile active"> |
| <span class="text-2xl">🏠</span> |
| <span class="text-xs">الرئيسية</span> |
| </a> |
| <a href="#" onclick="loadFeed('friends'); return false;" class="flex flex-col items-center text-gray-400 hover:text-[#ff2d55] transition nav-item-mobile"> |
| <span class="text-2xl">👥</span> |
| <span class="text-xs">الأصدقاء</span> |
| </a> |
| <a href="#" onclick="loadFeed('trending'); return false;" class="flex flex-col items-center text-gray-400 hover:text-[#ff2d55] transition nav-item-mobile"> |
| <span class="text-2xl">🔥</span> |
| <span class="text-xs">رائج</span> |
| </a> |
| <a href="#" onclick="openGlass('/glass/search'); return false;" class="flex flex-col items-center text-gray-400 hover:text-[#ff2d55] transition"> |
| <span class="text-2xl">🔍</span> |
| <span class="text-xs">بحث</span> |
| </a> |
| <a href="#" onclick="openGlass('/glass/profile/{{ session.user_id }}'); return false;" class="flex flex-col items-center text-gray-400 hover:text-[#ff2d55] transition"> |
| <span class="text-2xl">👤</span> |
| <span class="text-xs">حسابي</span> |
| </a> |
| </div> |
| |
| <!-- لوحة التعليقات --> |
| <div id="commentsPanel" class="comments-panel fixed bottom-0 left-0 right-0 lg:left-auto lg:right-64 lg:w-96 rounded-t-3xl lg:rounded-tr-none lg:rounded-l-3xl transform translate-y-full transition-transform duration-300 z-50 max-h-[80vh] flex flex-col"> |
| <div class="p-4 border-b border-gray-800 flex justify-between items-center"> |
| <h3 class="font-bold text-lg">💬 التعليقات</h3> |
| <button onclick="closeComments()" class="text-gray-400 hover:text-white text-2xl">✕</button> |
| </div> |
| <div id="commentsList" class="flex-1 overflow-y-auto p-4 space-y-4"></div> |
| <div class="p-4 border-t border-gray-800"> |
| <div class="flex gap-2"> |
| <input type="text" id="commentInput" placeholder="أضف تعليقاً..." |
| class="flex-1 bg-white/5 rounded-2xl px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-[#ff2d55]"> |
| <button onclick="sendComment()" class="bg-[#ff2d55] px-6 py-2 rounded-2xl font-bold">إرسال</button> |
| </div> |
| </div> |
| </div> |
| |
| <!-- طبقة الزجاج (Glass Layer) للملفات الشخصية --> |
| <div id="glassLayer" class="fixed inset-0 bg-black/90 backdrop-blur-xl z-[100] hidden overflow-y-auto"> |
| <div class="min-h-screen flex items-center justify-center p-4"> |
| <div class="relative w-full max-w-4xl"> |
| <button onclick="closeGlass()" class="absolute -top-2 -left-2 w-12 h-12 rounded-full bg-[#ff2d55] flex items-center justify-center text-2xl z-10"> |
| ✕ |
| </button> |
| <div id="glassContent" class="glass-sidebar rounded-3xl p-6"></div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| let currentPage = 1; |
| let loading = false; |
| let hasMore = true; |
| let currentFeed = 'for-you'; |
| let commentsVideoId = null; |
| let replyingTo = null; |
| let eventSource = null; |
| |
| const videoContainer = document.getElementById('videoContainer'); |
| const commentsPanel = document.getElementById('commentsPanel'); |
| const commentsList = document.getElementById('commentsList'); |
| const commentInput = document.getElementById('commentInput'); |
| const glassLayer = document.getElementById('glassLayer'); |
| const glassContent = document.getElementById('glassContent'); |
| |
| // تحميل الفيديوهات عند بدء الصفحة |
| loadVideos(); |
| |
| // التمرير اللانهائي |
| videoContainer.addEventListener('scroll', () => { |
| if (videoContainer.scrollTop + videoContainer.clientHeight >= videoContainer.scrollHeight - 100) { |
| if (!loading && hasMore) { |
| loadVideos(); |
| } |
| } |
| }); |
| |
| // النقر المزدوج لإعجاب سريع |
| videoContainer.addEventListener('dblclick', (e) => { |
| const videoItem = e.target.closest('.video-item'); |
| if (!videoItem) return; |
| |
| const videoId = videoItem.dataset.videoId; |
| if (videoId) { |
| likeVideo(videoId, null); |
| |
| // تأثير قلب |
| const heart = document.createElement('div'); |
| heart.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-7xl z-50 animate-bounce'; |
| heart.innerHTML = '❤️'; |
| heart.style.animation = 'bounce 1s ease'; |
| document.body.appendChild(heart); |
| setTimeout(() => heart.remove(), 1000); |
| } |
| }); |
| |
| function loadVideos() { |
| if (loading) return; |
| loading = true; |
| |
| fetch(`/api/${currentFeed}?page=${currentPage}`) |
| .then(res => res.json()) |
| .then(data => { |
| if (!data.videos || data.videos.length === 0) { |
| hasMore = false; |
| return; |
| } |
| renderVideos(data.videos); |
| currentPage = data.next_page || currentPage + 1; |
| hasMore = data.has_more !== false; |
| }) |
| .finally(() => { loading = false; }); |
| } |
| |
| function renderVideos(videos) { |
| videos.forEach(video => { |
| const container = document.createElement('div'); |
| container.className = 'video-item'; |
| container.dataset.videoId = video.id; |
| |
| const videoElem = document.createElement('video'); |
| videoElem.src = video.url; |
| videoElem.loop = true; |
| videoElem.muted = true; |
| videoElem.playsInline = true; |
| videoElem.className = 'w-full h-full object-cover'; |
| |
| // تشغيل/إيقاف عند المشاهدة |
| const observer = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting) { |
| videoElem.play().catch(() => {}); |
| videoElem.muted = false; |
| |
| // تسجيل مشاهدة |
| fetch(`/api/view/${video.id}`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({watch_time: 1}) |
| }); |
| |
| // تحديث شريط التقدم |
| updateProgressBar(video.id); |
| } else { |
| videoElem.pause(); |
| videoElem.muted = true; |
| } |
| }); |
| }, { threshold: 0.7 }); |
| observer.observe(videoElem); |
| |
| // معلومات الفيديو |
| const info = document.createElement('div'); |
| info.className = 'absolute bottom-0 right-0 left-0 p-6 bg-gradient-to-t from-black/90 via-black/50 to-transparent'; |
| info.innerHTML = ` |
| <div class="flex items-center gap-3 mb-2"> |
| <img src="${video.avatar_url}" class="w-12 h-12 rounded-2xl border-2 border-[#ff2d55] cursor-pointer" onclick="event.stopPropagation(); openGlass('/glass/profile/${video.user_id}')"> |
| <div> |
| <div class="flex items-center gap-2"> |
| <span class="font-bold">@${video.username}</span> |
| ${video.is_verified ? '<span class="text-[#ff2d55]">✓</span>' : ''} |
| ${video.role === 'vip' ? '<span class="vip-badge text-xs px-2 py-0.5 rounded-full">VIP</span>' : ''} |
| </div> |
| <div class="text-sm text-gray-300">${video.title || ''}</div> |
| </div> |
| </div> |
| <div class="flex items-center gap-2 text-sm"> |
| <span>❤️ ${video.likes_count || 0}</span> |
| <span>👁️ ${video.views || 0}</span> |
| <span>💬 ${video.comments_count || 0}</span> |
| </div> |
| `; |
| |
| // أزرار التفاعل |
| const actions = document.createElement('div'); |
| actions.className = 'absolute left-4 bottom-24 flex flex-col gap-4'; |
| actions.innerHTML = ` |
| <button onclick="likeVideo(${video.id}, this)" class="action-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl ${video.liked_by_user ? 'active' : ''}"> |
| ❤️ |
| </button> |
| <button onclick="openComments(${video.id})" class="action-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl"> |
| 💬 |
| </button> |
| <button onclick="saveVideo(${video.id}, this)" class="action-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl ${video.saved_by_user ? 'active' : ''}"> |
| 📁 |
| </button> |
| <button onclick="shareVideo(${video.id})" class="action-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl"> |
| ↗️ |
| </button> |
| `; |
| |
| // شريط التقدم |
| const progressBar = document.createElement('div'); |
| progressBar.className = 'absolute top-0 right-0 left-0 h-1 bg-gray-800'; |
| progressBar.innerHTML = '<div id="progress-${video.id}" class="h-full bg-[#ff2d55] transition-all duration-300" style="width: 0%"></div>'; |
| |
| container.appendChild(videoElem); |
| container.appendChild(progressBar); |
| container.appendChild(info); |
| container.appendChild(actions); |
| videoContainer.appendChild(container); |
| }); |
| } |
| |
| function updateProgressBar(videoId) { |
| let progress = 0; |
| const interval = setInterval(() => { |
| progress += 1; |
| const bar = document.getElementById(`progress-${videoId}`); |
| if (bar) { |
| bar.style.width = `${progress}%`; |
| } |
| if (progress >= 100) { |
| clearInterval(interval); |
| fetch(`/api/view/${videoId}`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({completed: true, watch_percentage: 100}) |
| }); |
| } |
| }, 1000); |
| } |
| |
| window.likeVideo = function(videoId, btn) { |
| const liked = btn ? btn.classList.contains('active') : false; |
| fetch((liked ? '/api/unlike/' : '/api/like/') + videoId, { method: 'POST' }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.status === 'ok' && btn) { |
| btn.classList.toggle('active'); |
| } |
| }); |
| }; |
| |
| window.saveVideo = function(videoId, btn) { |
| fetch(`/api/save/${videoId}`, { method: 'POST' }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.status === 'ok' && btn) { |
| btn.classList.toggle('active'); |
| } |
| }); |
| }; |
| |
| window.shareVideo = function(videoId) { |
| if (navigator.share) { |
| navigator.share({ |
| title: 'ARC Video', |
| text: 'شاهد هذا الفيديو الرائع!', |
| url: window.location.origin + '/video/' + videoId |
| }); |
| } else { |
| navigator.clipboard.writeText(window.location.origin + '/video/' + videoId); |
| alert('تم نسخ الرابط'); |
| } |
| }; |
| |
| window.openComments = function(videoId) { |
| commentsVideoId = videoId; |
| replyingTo = null; |
| commentsPanel.classList.remove('translate-y-full'); |
| commentsPanel.classList.add('translate-y-0'); |
| loadComments(videoId); |
| }; |
| |
| window.closeComments = function() { |
| commentsPanel.classList.add('translate-y-full'); |
| commentsPanel.classList.remove('translate-y-0'); |
| commentsVideoId = null; |
| }; |
| |
| function loadComments(videoId) { |
| fetch(`/api/comments/${videoId}`) |
| .then(res => res.json()) |
| .then(data => { |
| commentsList.innerHTML = ''; |
| if (data.comments && data.comments.length) { |
| data.comments.forEach(c => { |
| commentsList.appendChild(renderComment(c)); |
| }); |
| } else { |
| commentsList.innerHTML = '<p class="text-center text-gray-500 py-8">لا توجد تعليقات بعد</p>'; |
| } |
| }); |
| } |
| |
| function renderComment(c) { |
| const div = document.createElement('div'); |
| div.className = 'border-b border-gray-800 pb-4'; |
| div.innerHTML = ` |
| <div class="flex items-center gap-2 mb-2"> |
| <img src="${c.avatar_url}" class="w-8 h-8 rounded-full"> |
| <span class="font-bold">@${c.username}</span> |
| <span class="text-xs text-gray-500">${new Date(c.timestamp).toLocaleString('ar')}</span> |
| </div> |
| <p class="text-gray-300 mr-10">${c.comment_text}</p> |
| <div class="flex gap-4 mt-2 mr-10"> |
| <button onclick="likeComment(${c.id})" class="text-sm text-gray-400 hover:text-[#ff2d55] flex items-center gap-1"> |
| ❤️ <span>${c.likes_count || 0}</span> |
| </button> |
| <button onclick="replyToComment(${c.id})" class="text-sm text-gray-400 hover:text-[#ff2d55]"> |
| ↩️ رد |
| </button> |
| </div> |
| ${c.replies && c.replies.length ? ` |
| <div class="mr-6 mt-2 space-y-2"> |
| ${c.replies.map(r => renderComment(r)).map(el => el.outerHTML).join('')} |
| </div> |
| ` : ''} |
| `; |
| return div; |
| } |
| |
| window.likeComment = function(commentId) { |
| fetch(`/api/comment/like/${commentId}`, { method: 'POST' }) |
| .then(() => { if (commentsVideoId) loadComments(commentsVideoId); }); |
| }; |
| |
| window.replyToComment = function(parentId) { |
| replyingTo = parentId; |
| commentInput.focus(); |
| commentInput.placeholder = 'اكتب رداً...'; |
| }; |
| |
| window.sendComment = function() { |
| const text = commentInput.value.trim(); |
| if (!text || !commentsVideoId) return; |
| |
| fetch(`/api/comment/${commentsVideoId}`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ comment: text, parent_id: replyingTo }) |
| }).then(() => { |
| commentInput.value = ''; |
| replyingTo = null; |
| commentInput.placeholder = 'أضف تعليقاً...'; |
| loadComments(commentsVideoId); |
| }); |
| }; |
| |
| function loadFeed(feed) { |
| currentFeed = feed; |
| currentPage = 1; |
| hasMore = true; |
| videoContainer.innerHTML = ''; |
| loadVideos(); |
| |
| // تحديث العنصر النشط في القائمة |
| document.querySelectorAll('.nav-item, .nav-item-mobile').forEach(item => { |
| item.classList.remove('active', 'bg-[#ff2d55]'); |
| }); |
| event.target.closest('a').classList.add('active'); |
| if (event.target.closest('a').classList.contains('nav-item')) { |
| event.target.closest('a').classList.add('bg-[#ff2d55]'); |
| } |
| } |
| |
| window.loadFeed = loadFeed; |
| |
| function openGlass(url) { |
| fetch(url) |
| .then(res => res.text()) |
| .then(html => { |
| glassContent.innerHTML = html; |
| glassLayer.style.display = 'block'; |
| }); |
| } |
| |
| window.openGlass = openGlass; |
| |
| function closeGlass() { |
| glassLayer.style.display = 'none'; |
| } |
| |
| window.closeGlass = closeGlass; |
| |
| // إشعارات مباشرة |
| function connectNotifications() { |
| if (window.EventSource) { |
| eventSource = new EventSource('/api/notifications/stream'); |
| |
| eventSource.addEventListener('notification', function(e) { |
| const data = JSON.parse(e.data); |
| showNotification(data); |
| updateNotificationBadge(); |
| }); |
| |
| eventSource.addEventListener('ping', function() { |
| // للحفاظ على الاتصال |
| }); |
| } |
| } |
| |
| function showNotification(notification) { |
| // إنشاء عنصر إشعار |
| const notif = document.createElement('div'); |
| notif.className = 'fixed top-4 left-4 glass-sidebar rounded-2xl p-4 max-w-sm z-50 slide-up'; |
| notif.innerHTML = ` |
| <div class="flex gap-3"> |
| <div class="text-2xl">${notification.type === 'like' ? '❤️' : notification.type === 'comment' ? '💬' : '🔔'}</div> |
| <div> |
| <p class="text-sm">${notification.content}</p> |
| <p class="text-xs text-gray-400 mt-1">الآن</p> |
| </div> |
| </div> |
| `; |
| document.body.appendChild(notif); |
| setTimeout(() => notif.remove(), 5000); |
| } |
| |
| function updateNotificationBadge() { |
| fetch('/api/notifications/count') |
| .then(res => res.json()) |
| .then(data => { |
| const badge = document.getElementById('sidebarNotifBadge'); |
| if (data.count > 0) { |
| badge.textContent = data.count; |
| badge.classList.remove('hidden'); |
| } else { |
| badge.classList.add('hidden'); |
| } |
| }); |
| } |
| |
| function openNotifications() { |
| openGlass('/glass/notifications'); |
| } |
| |
| // بدء الاتصال المباشر للإشعارات |
| connectNotifications(); |
| updateNotificationBadge(); |
| setInterval(updateNotificationBadge, 30000); |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| PROFILE_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>@{{ user.username }} - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .gradient-text { |
| background: linear-gradient(135deg, #ff2d55, #ff8a5c); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| } |
| .vip-badge { |
| background: linear-gradient(135deg, #FFD700, #FFA500); |
| color: #000; |
| } |
| .gold-badge { |
| background: linear-gradient(135deg, #FFD700, #B8860B); |
| color: #000; |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white"> |
| <!-- صورة الغلاف --> |
| <div class="h-64 bg-cover bg-center relative" style="background-image: url('/avatars/{{ user.cover_image or 'default_cover.jpg' }}')"> |
| <div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div> |
| <a href="/" class="absolute top-4 right-4 glass-card px-4 py-2 rounded-2xl text-white hover:bg-[#ff2d55] transition z-10"> |
| ← العودة |
| </a> |
| </div> |
| |
| <div class="max-w-6xl mx-auto px-4 -mt-20 relative z-20"> |
| <!-- معلومات المستخدم --> |
| <div class="glass-card rounded-3xl p-6 mb-6"> |
| <div class="flex flex-col md:flex-row items-start md:items-center gap-6"> |
| <img src="/avatars/{{ user.avatar }}" class="w-28 h-28 rounded-3xl border-4 border-[#ff2d55] object-cover"> |
| |
| <div class="flex-1"> |
| <div class="flex items-center gap-3 flex-wrap"> |
| <h1 class="text-3xl font-bold">@{{ user.username }}</h1> |
| {% if user.is_verified %} |
| <span class="bg-[#ff2d55] px-3 py-1 rounded-full text-sm">✓ متحقق</span> |
| {% endif %} |
| {% if user.role == 'vip' %} |
| <span class="vip-badge px-3 py-1 rounded-full text-sm font-bold">VIP</span> |
| {% elif user.role == 'vip_gold' %} |
| <span class="gold-badge px-3 py-1 rounded-full text-sm font-bold">VIP GOLD</span> |
| {% endif %} |
| </div> |
| |
| <p class="text-gray-300 mt-2">{{ user.bio or 'لا توجد سيرة ذاتية' }}</p> |
| |
| <div class="flex gap-6 mt-4"> |
| <div class="text-center"> |
| <div class="text-2xl font-bold">{{ stats.videos or 0 }}</div> |
| <div class="text-sm text-gray-400">فيديو</div> |
| </div> |
| <div class="text-center"> |
| <div class="text-2xl font-bold">{{ followers_count }}</div> |
| <div class="text-sm text-gray-400">متابع</div> |
| </div> |
| <div class="text-center"> |
| <div class="text-2xl font-bold">{{ following_count }}</div> |
| <div class="text-sm text-gray-400">يتابع</div> |
| </div> |
| <div class="text-center"> |
| <div class="text-2xl font-bold">{{ stats.total_views or 0 }}</div> |
| <div class="text-sm text-gray-400">مشاهدة</div> |
| </div> |
| </div> |
| |
| <div class="flex items-center gap-4 mt-4"> |
| <div class="flex items-center gap-2 bg-white/5 px-4 py-2 rounded-2xl"> |
| <span class="text-[#ff2d55]">⭐</span> |
| <span>المستوى {{ stats.level }}</span> |
| </div> |
| <div class="flex items-center gap-2 bg-white/5 px-4 py-2 rounded-2xl"> |
| <span class="text-[#ff2d55]">💎</span> |
| <span>{{ stats.xp }} XP</span> |
| </div> |
| <div class="flex items-center gap-2 bg-white/5 px-4 py-2 rounded-2xl"> |
| <span class="text-[#ff2d55]">🪙</span> |
| <span>{{ stats.coins }} عملة</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="flex gap-3"> |
| {% if session.user_id == user.id %} |
| <a href="/settings" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| ⚙️ الإعدادات |
| </a> |
| <a href="/glass/saved" onclick="openGlass('/glass/saved'); return false;" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| 📁 المحفوظة |
| </a> |
| {% else %} |
| <button onclick="toggleFollow({{ user.id }})" class="glass-card px-8 py-3 rounded-2xl hover:bg-[#ff2d55] transition font-bold" id="followBtn"> |
| {{ 'إلغاء المتابعة' if is_following else 'متابعة' }} |
| </button> |
| <button onclick="openChat({{ user.id }})" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| 💬 مراسلة |
| </button> |
| {% endif %} |
| </div> |
| </div> |
| </div> |
| |
| <!-- إنجازات المستخدم --> |
| {% if stats.achievements %} |
| <div class="glass-card rounded-3xl p-6 mb-6"> |
| <h2 class="text-xl font-bold mb-4">🏆 الإنجازات</h2> |
| <div class="flex gap-3 flex-wrap"> |
| {% for achievement in stats.achievements %} |
| <span class="bg-[#ff2d55]/20 border border-[#ff2d55] px-4 py-2 rounded-2xl text-sm"> |
| {{ achievement }} |
| </span> |
| {% endfor %} |
| </div> |
| </div> |
| {% endif %} |
| |
| <!-- فيديوهات المستخدم --> |
| <div class="glass-card rounded-3xl p-6"> |
| <h2 class="text-xl font-bold mb-4">📹 فيديوهات @{{ user.username }}</h2> |
| |
| {% if videos %} |
| <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> |
| {% for video in videos %} |
| <a href="/video/{{ video.id }}" class="group relative aspect-[9/16] rounded-2xl overflow-hidden"> |
| <video src="{{ video.url }}" class="w-full h-full object-cover group-hover:scale-105 transition duration-300"></video> |
| <div class="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent"></div> |
| <div class="absolute bottom-2 right-2 left-2"> |
| <p class="text-sm font-bold truncate">{{ video.title or 'بلا عنوان' }}</p> |
| <div class="flex gap-2 text-xs text-gray-300 mt-1"> |
| <span>❤️ {{ video.likes_count }}</span> |
| <span>👁️ {{ video.views }}</span> |
| </div> |
| </div> |
| </a> |
| {% endfor %} |
| </div> |
| {% else %} |
| <p class="text-center text-gray-400 py-12">لا توجد فيديوهات بعد</p> |
| {% endif %} |
| </div> |
| </div> |
| |
| <script> |
| function toggleFollow(userId) { |
| const btn = document.getElementById('followBtn'); |
| const isFollowing = btn.innerText.includes('إلغاء'); |
| |
| fetch((isFollowing ? '/unfollow/' : '/follow/') + userId, { method: 'POST' }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.status === 'ok') { |
| btn.innerText = isFollowing ? 'متابعة' : 'إلغاء المتابعة'; |
| location.reload(); |
| } |
| }); |
| } |
| |
| function openChat(userId) { |
| window.location.href = '/messages/' + userId; |
| } |
| |
| function openGlass(url) { |
| window.location.href = url; |
| } |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| SETTINGS_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>الإعدادات - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| } |
| .input-glass { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| transition: all 0.3s ease; |
| } |
| .input-glass:focus { |
| border-color: #ff2d55; |
| box-shadow: 0 0 0 2px rgba(255, 45, 85, 0.2); |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white p-4"> |
| <div class="max-w-4xl mx-auto"> |
| <a href="/profile/{{ session.user_id }}" class="inline-block glass-card px-4 py-2 rounded-2xl mb-6 hover:bg-[#ff2d55] transition"> |
| ← العودة للملف الشخصي |
| </a> |
| |
| <div class="glass-card rounded-3xl p-8"> |
| <h1 class="text-3xl font-bold mb-8 text-center gradient-text">الإعدادات</h1> |
| |
| <form method="post" enctype="multipart/form-data" class="space-y-8"> |
| <!-- الصور الشخصية --> |
| <div class="flex flex-col items-center gap-4"> |
| <div class="flex gap-6 flex-wrap justify-center"> |
| <div class="text-center"> |
| <img src="/avatars/{{ user.avatar }}" id="avatarPreview" class="w-32 h-32 rounded-3xl border-4 border-[#ff2d55] object-cover mb-2"> |
| <label class="cursor-pointer glass-card px-4 py-2 rounded-2xl text-sm hover:bg-[#ff2d55] transition"> |
| تغيير الصورة |
| <input type="file" name="avatar" accept="image/*" class="hidden" onchange="previewImage(this, 'avatarPreview')"> |
| </label> |
| </div> |
| <div class="text-center"> |
| <img src="/avatars/{{ user.cover_image or 'default_cover.jpg' }}" id="coverPreview" class="w-48 h-32 rounded-2xl border-4 border-[#ff2d55] object-cover mb-2"> |
| <label class="cursor-pointer glass-card px-4 py-2 rounded-2xl text-sm hover:bg-[#ff2d55] transition"> |
| تغيير الغلاف |
| <input type="file" name="cover" accept="image/*" class="hidden" onchange="previewImage(this, 'coverPreview')"> |
| </label> |
| </div> |
| </div> |
| </div> |
| |
| <!-- المعلومات الشخصية --> |
| <div class="grid md:grid-cols-2 gap-6"> |
| <div> |
| <label class="block text-gray-300 mb-2">السيرة الذاتية</label> |
| <textarea name="bio" rows="3" class="w-full input-glass rounded-2xl p-3 text-white">{{ user.bio or '' }}</textarea> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">رقم الهاتف</label> |
| <input type="tel" name="phone" value="{{ user.phone or '' }}" class="w-full input-glass rounded-2xl p-3 text-white"> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">الجنس</label> |
| <select name="gender" class="w-full input-glass rounded-2xl p-3 text-white"> |
| <option value="">اختر</option> |
| <option value="male" {% if user.gender == 'male' %}selected{% endif %}>ذكر</option> |
| <option value="female" {% if user.gender == 'female' %}selected{% endif %}>أنثى</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">الدولة</label> |
| <input type="text" name="country" value="{{ user.country or '' }}" class="w-full input-glass rounded-2xl p-3 text-white"> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">تاريخ الميلاد</label> |
| <input type="date" name="birth_date" value="{{ user.birth_date or '' }}" class="w-full input-glass rounded-2xl p-3 text-white"> |
| </div> |
| </div> |
| |
| <!-- إعدادات الخصوصية --> |
| <div class="border-t border-gray-800 pt-6"> |
| <h2 class="text-xl font-bold mb-4">🔒 الخصوصية</h2> |
| <div class="grid md:grid-cols-3 gap-4"> |
| <div> |
| <label class="block text-gray-300 mb-2">الملف الشخصي</label> |
| <select name="privacy_profile" class="w-full input-glass rounded-2xl p-3"> |
| <option value="public" {% if settings.privacy_profile == 'public' %}selected{% endif %}>عام</option> |
| <option value="followers" {% if settings.privacy_profile == 'followers' %}selected{% endif %}>المتابعون فقط</option> |
| <option value="private" {% if settings.privacy_profile == 'private' %}selected{% endif %}>خاص</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">الفيديوهات</label> |
| <select name="privacy_videos" class="w-full input-glass rounded-2xl p-3"> |
| <option value="public" {% if settings.privacy_videos == 'public' %}selected{% endif %}>عام</option> |
| <option value="followers" {% if settings.privacy_videos == 'followers' %}selected{% endif %}>المتابعون فقط</option> |
| <option value="private" {% if settings.privacy_videos == 'private' %}selected{% endif %}>خاص</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">الإعجابات</label> |
| <select name="privacy_likes" class="w-full input-glass rounded-2xl p-3"> |
| <option value="public" {% if settings.privacy_likes == 'public' %}selected{% endif %}>عام</option> |
| <option value="followers" {% if settings.privacy_likes == 'followers' %}selected{% endif %}>المتابعون فقط</option> |
| <option value="private" {% if settings.privacy_likes == 'private' %}selected{% endif %}>خاص</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| |
| <!-- إعدادات الإشعارات --> |
| <div class="border-t border-gray-800 pt-6"> |
| <h2 class="text-xl font-bold mb-4">🔔 الإشعارات</h2> |
| <div class="grid md:grid-cols-2 gap-4"> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="notifications_likes" {% if settings.notifications_likes %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>الإعجابات</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="notifications_comments" {% if settings.notifications_comments %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>التعليقات</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="notifications_follows" {% if settings.notifications_follows %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>المتابعات</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="notifications_messages" {% if settings.notifications_messages %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>الرسائل</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="notifications_live" {% if settings.notifications_live %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>البث المباشر</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="notifications_gifts" {% if settings.notifications_gifts %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>الهدايا</span> |
| </label> |
| </div> |
| </div> |
| |
| <!-- التفضيلات --> |
| <div class="border-t border-gray-800 pt-6"> |
| <h2 class="text-xl font-bold mb-4">⚙️ التفضيلات</h2> |
| <div class="grid md:grid-cols-2 gap-6"> |
| <div> |
| <label class="block text-gray-300 mb-2">الوضع الليلي</label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="dark_mode" {% if settings.dark_mode %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>تفعيل</span> |
| </label> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">اللغة</label> |
| <select name="language" class="w-full input-glass rounded-2xl p-3"> |
| <option value="ar" {% if settings.language == 'ar' %}selected{% endif %}>العربية</option> |
| <option value="en" {% if settings.language == 'en' %}selected{% endif %}>English</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">لغة المحتوى</label> |
| <select name="content_language" class="w-full input-glass rounded-2xl p-3"> |
| <option value="ar" {% if settings.content_language == 'ar' %}selected{% endif %}>العربية</option> |
| <option value="en" {% if settings.content_language == 'en' %}selected{% endif %}>English</option> |
| <option value="all" {% if settings.content_language == 'all' %}selected{% endif %}>الكل</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">التشغيل التلقائي</label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="autoplay" {% if settings.autoplay %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>تفعيل</span> |
| </label> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">توفير البيانات</label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="save_data" {% if settings.save_data %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>تفعيل</span> |
| </label> |
| </div> |
| <div> |
| <label class="block text-gray-300 mb-2">السماح بالتحميل</label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="allow_download" {% if settings.allow_download %}checked{% endif %} class="form-checkbox text-[#ff2d55]"> |
| <span>تفعيل</span> |
| </label> |
| </div> |
| </div> |
| </div> |
| |
| <!-- الأمان --> |
| <div class="border-t border-gray-800 pt-6"> |
| <h2 class="text-xl font-bold mb-4">🔐 الأمان</h2> |
| <a href="/2fa/setup" class="inline-block glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| إعداد المصادقة الثنائية |
| </a> |
| </div> |
| |
| <button type="submit" class="w-full btn-primary text-white font-bold py-4 px-4 rounded-2xl text-lg"> |
| حفظ التغييرات |
| </button> |
| </form> |
| </div> |
| </div> |
| |
| <script> |
| function previewImage(input, previewId) { |
| if (input.files && input.files[0]) { |
| const reader = new FileReader(); |
| reader.onload = e => document.getElementById(previewId).src = e.target.result; |
| reader.readAsDataURL(input.files[0]); |
| } |
| } |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| UPLOAD_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>رفع فيديو - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .btn-primary { |
| background: linear-gradient(135deg, #ff2d55, #ff4d6d); |
| } |
| .input-glass { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| transition: all 0.3s ease; |
| } |
| .input-glass:focus { |
| border-color: #ff2d55; |
| box-shadow: 0 0 0 2px rgba(255, 45, 85, 0.2); |
| } |
| .upload-area { |
| border: 2px dashed rgba(255, 45, 85, 0.5); |
| transition: all 0.3s ease; |
| } |
| .upload-area:hover, .upload-area.dragover { |
| border-color: #ff2d55; |
| background: rgba(255, 45, 85, 0.1); |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white p-4"> |
| <div class="max-w-4xl mx-auto"> |
| <a href="/" class="inline-block glass-card px-4 py-2 rounded-2xl mb-6 hover:bg-[#ff2d55] transition"> |
| ← العودة للرئيسية |
| </a> |
| |
| <div class="glass-card rounded-3xl p-8"> |
| <h1 class="text-3xl font-bold mb-8 text-center gradient-text">رفع فيديو جديد</h1> |
| |
| <!-- معاينة الكاميرا --> |
| <div class="mb-8"> |
| <div class="relative"> |
| <video id="cameraPreview" autoplay muted class="w-full rounded-3xl bg-black aspect-video object-cover hidden"></video> |
| <video id="recordedPreview" controls class="w-full rounded-3xl bg-black aspect-video object-cover hidden"></video> |
| <div id="cameraPlaceholder" class="upload-area rounded-3xl aspect-video flex flex-col items-center justify-center"> |
| <div class="text-6xl mb-4">📸</div> |
| <p class="text-gray-400">اضغط لفتح الكاميرا أو اسحب الفيديو</p> |
| </div> |
| </div> |
| |
| <div class="flex gap-3 mt-4 justify-center flex-wrap"> |
| <button type="button" onclick="startCamera()" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| 📷 فتح الكاميرا |
| </button> |
| <button type="button" onclick="switchCamera()" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| 🔄 تبديل الكاميرا |
| </button> |
| <button type="button" onclick="startRecording()" id="startRecordBtn" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition"> |
| ⏺ تسجيل |
| </button> |
| <button type="button" onclick="stopRecording()" id="stopRecordBtn" class="glass-card px-6 py-3 rounded-2xl hover:bg-[#ff2d55] transition" disabled> |
| ⏹ إيقاف |
| </button> |
| </div> |
| </div> |
| |
| <form method="post" enctype="multipart/form-data" id="uploadForm" class="space-y-6"> |
| <div> |
| <label class="block text-gray-300 mb-2">عنوان الفيديو</label> |
| <input type="text" name="title" required class="w-full input-glass rounded-2xl p-3 text-white"> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2">وصف الفيديو</label> |
| <textarea name="description" rows="4" class="w-full input-glass rounded-2xl p-3 text-white" placeholder="أضف وصفاً مع وسوم #تحدي #مرح"></textarea> |
| </div> |
| |
| <div class="grid md:grid-cols-2 gap-6"> |
| <div> |
| <label class="block text-gray-300 mb-2">الموسيقى</label> |
| <select name="music_id" class="w-full input-glass rounded-2xl p-3 text-white"> |
| <option value="">بدون موسيقى</option> |
| {% for m in music %} |
| <option value="{{ m.id }}">{{ m.title }} - {{ m.artist }}</option> |
| {% endfor %} |
| </select> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2">تحدي</label> |
| <select name="challenge_id" class="w-full input-glass rounded-2xl p-3 text-white"> |
| <option value="">بدون تحدي</option> |
| {% for c in challenges %} |
| <option value="{{ c.id }}">{{ c.title }} (🏆 {{ c.prize_coins }} عملة)</option> |
| {% endfor %} |
| </select> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-gray-300 mb-2">إعدادات الخصوصية</label> |
| <select name="visibility" class="w-full input-glass rounded-2xl p-3 text-white"> |
| <option value="public">عام</option> |
| <option value="friends">الأصدقاء فقط</option> |
| <option value="private">خاص</option> |
| </select> |
| </div> |
| |
| <div class="grid md:grid-cols-3 gap-4"> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="allow_comments" checked class="form-checkbox text-[#ff2d55]"> |
| <span>السماح بالتعليقات</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="allow_duet" checked class="form-checkbox text-[#ff2d55]"> |
| <span>السماح بالثنائي</span> |
| </label> |
| <label class="flex items-center gap-2"> |
| <input type="checkbox" name="allow_stitch" checked class="form-checkbox text-[#ff2d55]"> |
| <span>السماح بالمونتاج</span> |
| </label> |
| </div> |
| |
| <div class="upload-area rounded-3xl p-8 text-center" id="fileDropArea"> |
| <input type="file" name="video" accept="video/*" id="videoFile" class="hidden" onchange="handleFileSelect(this)"> |
| <div class="text-4xl mb-2">🎬</div> |
| <p class="text-gray-400 mb-2">اسحب الفيديو هنا أو</p> |
| <button type="button" onclick="document.getElementById('videoFile').click()" class="btn-primary text-white px-6 py-2 rounded-2xl"> |
| اختر ملف |
| </button> |
| <p id="fileName" class="text-sm text-gray-500 mt-2"></p> |
| </div> |
| |
| <button type="submit" class="w-full btn-primary text-white font-bold py-4 px-4 rounded-2xl text-lg"> |
| رفع الفيديو |
| </button> |
| </form> |
| </div> |
| </div> |
| |
| <script> |
| let currentStream = null; |
| let useFrontCamera = true; |
| let mediaRecorder = null; |
| let recordedChunks = []; |
| |
| const cameraPreview = document.getElementById('cameraPreview'); |
| const recordedPreview = document.getElementById('recordedPreview'); |
| const cameraPlaceholder = document.getElementById('cameraPlaceholder'); |
| const startRecordBtn = document.getElementById('startRecordBtn'); |
| const stopRecordBtn = document.getElementById('stopRecordBtn'); |
| |
| async function startCamera() { |
| try { |
| if (currentStream) { |
| currentStream.getTracks().forEach(track => track.stop()); |
| } |
| |
| const constraints = { |
| video: { facingMode: useFrontCamera ? 'user' : 'environment' }, |
| audio: true |
| }; |
| |
| currentStream = await navigator.mediaDevices.getUserMedia(constraints); |
| cameraPreview.srcObject = currentStream; |
| cameraPreview.classList.remove('hidden'); |
| recordedPreview.classList.add('hidden'); |
| cameraPlaceholder.classList.add('hidden'); |
| } catch (err) { |
| alert('لا يمكن الوصول للكاميرا: ' + err.message); |
| } |
| } |
| |
| function switchCamera() { |
| useFrontCamera = !useFrontCamera; |
| startCamera(); |
| } |
| |
| function startRecording() { |
| if (!currentStream) { |
| alert('يرجى تشغيل الكاميرا أولاً'); |
| return; |
| } |
| |
| recordedChunks = []; |
| mediaRecorder = new MediaRecorder(currentStream); |
| |
| mediaRecorder.ondataavailable = event => { |
| if (event.data.size > 0) recordedChunks.push(event.data); |
| }; |
| |
| mediaRecorder.onstop = () => { |
| const blob = new Blob(recordedChunks, { type: 'video/mp4' }); |
| const url = URL.createObjectURL(blob); |
| recordedPreview.src = url; |
| recordedPreview.classList.remove('hidden'); |
| cameraPreview.classList.add('hidden'); |
| |
| // إنشاء ملف للرفع |
| const file = new File([blob], 'recording.mp4', { type: 'video/mp4' }); |
| const dataTransfer = new DataTransfer(); |
| dataTransfer.items.add(file); |
| document.getElementById('videoFile').files = dataTransfer.files; |
| document.getElementById('fileName').textContent = 'recording.mp4'; |
| }; |
| |
| mediaRecorder.start(); |
| startRecordBtn.disabled = true; |
| stopRecordBtn.disabled = false; |
| } |
| |
| function stopRecording() { |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { |
| mediaRecorder.stop(); |
| startRecordBtn.disabled = false; |
| stopRecordBtn.disabled = true; |
| } |
| } |
| |
| function handleFileSelect(input) { |
| if (input.files && input.files[0]) { |
| document.getElementById('fileName').textContent = input.files[0].name; |
| |
| // معاينة الفيديو |
| const url = URL.createObjectURL(input.files[0]); |
| recordedPreview.src = url; |
| recordedPreview.classList.remove('hidden'); |
| cameraPreview.classList.add('hidden'); |
| cameraPlaceholder.classList.add('hidden'); |
| } |
| } |
| |
| // سحب وإفلات |
| const dropArea = document.getElementById('fileDropArea'); |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| dropArea.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| dropArea.addEventListener(eventName, () => dropArea.classList.add('dragover'), false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| dropArea.addEventListener(eventName, () => dropArea.classList.remove('dragover'), false); |
| }); |
| |
| dropArea.addEventListener('drop', (e) => { |
| const dt = e.dataTransfer; |
| const files = dt.files; |
| |
| if (files.length > 0) { |
| document.getElementById('videoFile').files = files; |
| handleFileSelect(document.getElementById('videoFile')); |
| } |
| }); |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| SEARCH_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>بحث - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .input-glass { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white p-4"> |
| <div class="max-w-6xl mx-auto"> |
| <a href="/" class="inline-block glass-card px-4 py-2 rounded-2xl mb-6 hover:bg-[#ff2d55] transition"> |
| ← العودة للرئيسية |
| </a> |
| |
| <div class="glass-card rounded-3xl p-6 mb-6"> |
| <div class="flex gap-4"> |
| <input type="text" id="searchInput" placeholder="ابحث عن فيديوهات، مستخدمين، هاشتاغات..." |
| class="flex-1 input-glass rounded-2xl px-6 py-4 text-white text-lg focus:outline-none focus:ring-2 focus:ring-[#ff2d55]" |
| value="{{ request.args.get('q', '') }}"> |
| <button onclick="performSearch()" class="btn-primary px-8 py-4 rounded-2xl font-bold"> |
| بحث |
| </button> |
| </div> |
| |
| <div class="flex gap-2 mt-4"> |
| <button onclick="setType('videos')" class="px-4 py-2 rounded-2xl bg-white/5 hover:bg-[#ff2d55] transition" id="typeVideos">📹 فيديوهات</button> |
| <button onclick="setType('users')" class="px-4 py-2 rounded-2xl bg-white/5 hover:bg-[#ff2d55] transition" id="typeUsers">👤 مستخدمين</button> |
| <button onclick="setType('hashtags')" class="px-4 py-2 rounded-2xl bg-white/5 hover:bg-[#ff2d55] transition" id="typeHashtags"># هاشتاغات</button> |
| </div> |
| </div> |
| |
| <div id="results" class="space-y-6"></div> |
| <div id="loading" class="text-center py-8 hidden"> |
| <div class="inline-block animate-spin text-4xl">⏳</div> |
| </div> |
| </div> |
| |
| <script> |
| let currentType = 'videos'; |
| let currentPage = 1; |
| let currentQuery = ''; |
| let loading = false; |
| |
| document.getElementById('searchInput').addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') performSearch(); |
| }); |
| |
| function setType(type) { |
| currentType = type; |
| document.querySelectorAll('[id^="type"]').forEach(btn => { |
| btn.classList.remove('bg-[#ff2d55]'); |
| }); |
| document.getElementById(`type${type.charAt(0).toUpperCase() + type.slice(1)}`).classList.add('bg-[#ff2d55]'); |
| performSearch(); |
| } |
| |
| function performSearch(reset = true) { |
| const query = document.getElementById('searchInput').value.trim(); |
| if (!query) return; |
| |
| currentQuery = query; |
| if (reset) { |
| currentPage = 1; |
| document.getElementById('results').innerHTML = ''; |
| } |
| |
| loadResults(); |
| } |
| |
| function loadResults() { |
| if (loading) return; |
| loading = true; |
| document.getElementById('loading').classList.remove('hidden'); |
| |
| fetch(`/api/search?q=${encodeURIComponent(currentQuery)}&type=${currentType}&page=${currentPage}`) |
| .then(res => res.json()) |
| .then(data => { |
| document.getElementById('loading').classList.add('hidden'); |
| |
| if (currentType === 'videos') { |
| renderVideos(data.videos); |
| } else if (currentType === 'users') { |
| renderUsers(data.users); |
| } else if (currentType === 'hashtags') { |
| renderHashtags(data.hashtags); |
| } |
| |
| if (data.next_page) { |
| currentPage = data.next_page; |
| } |
| |
| loading = false; |
| }); |
| } |
| |
| function renderVideos(videos) { |
| const container = document.getElementById('results'); |
| |
| if (videos.length === 0 && currentPage === 1) { |
| container.innerHTML = '<p class="text-center text-gray-400 py-12">لا توجد نتائج</p>'; |
| return; |
| } |
| |
| const grid = document.createElement('div'); |
| grid.className = 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4'; |
| |
| videos.forEach(v => { |
| const item = document.createElement('a'); |
| item.href = `/video/${v.id}`; |
| item.className = 'group relative aspect-[9/16] rounded-2xl overflow-hidden'; |
| item.innerHTML = ` |
| <video src="${v.url}" class="w-full h-full object-cover group-hover:scale-105 transition"></video> |
| <div class="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent"></div> |
| <div class="absolute bottom-2 right-2 left-2"> |
| <div class="flex items-center gap-2"> |
| <img src="${v.avatar_url}" class="w-6 h-6 rounded-full border border-[#ff2d55]"> |
| <span class="text-sm">@${v.username}</span> |
| </div> |
| <div class="flex gap-2 text-xs mt-1"> |
| <span>❤️ ${v.likes_count || 0}</span> |
| <span>👁️ ${v.views || 0}</span> |
| </div> |
| </div> |
| `; |
| grid.appendChild(item); |
| }); |
| |
| container.appendChild(grid); |
| } |
| |
| function renderUsers(users) { |
| const container = document.getElementById('results'); |
| |
| if (users.length === 0 && currentPage === 1) { |
| container.innerHTML = '<p class="text-center text-gray-400 py-12">لا توجد نتائج</p>'; |
| return; |
| } |
| |
| users.forEach(u => { |
| const item = document.createElement('div'); |
| item.className = 'glass-card rounded-2xl p-4 flex items-center gap-4'; |
| item.innerHTML = ` |
| <img src="${u.avatar_url}" class="w-16 h-16 rounded-2xl object-cover border-2 border-[#ff2d55]"> |
| <div class="flex-1"> |
| <div class="flex items-center gap-2"> |
| <span class="font-bold">@${u.username}</span> |
| ${u.is_verified ? '<span class="text-[#ff2d55]">✓</span>' : ''} |
| </div> |
| <p class="text-sm text-gray-400">${u.bio || ''}</p> |
| <div class="text-xs text-gray-500 mt-1">${u.total_followers || 0} متابع</div> |
| </div> |
| <button onclick="window.location.href='/profile/${u.id}'" class="glass-card px-4 py-2 rounded-2xl hover:bg-[#ff2d55] transition"> |
| عرض |
| </button> |
| `; |
| container.appendChild(item); |
| }); |
| } |
| |
| function renderHashtags(hashtags) { |
| const container = document.getElementById('results'); |
| |
| if (hashtags.length === 0 && currentPage === 1) { |
| container.innerHTML = '<p class="text-center text-gray-400 py-12">لا توجد نتائج</p>'; |
| return; |
| } |
| |
| const grid = document.createElement('div'); |
| grid.className = 'grid md:grid-cols-2 lg:grid-cols-3 gap-4'; |
| |
| hashtags.forEach(h => { |
| const item = document.createElement('a'); |
| item.href = `/search?q=%23${h.tag}`; |
| item.className = 'glass-card rounded-2xl p-6 text-center hover:border-[#ff2d55] transition'; |
| item.innerHTML = ` |
| <div class="text-4xl mb-2">#</div> |
| <div class="text-xl font-bold text-[#ff2d55]">#${h.tag}</div> |
| <div class="text-sm text-gray-400 mt-2">${h.usage_count || 0} فيديو</div> |
| <div class="text-xs text-gray-500">${h.total_views || 0} مشاهدة</div> |
| `; |
| grid.appendChild(item); |
| }); |
| |
| container.appendChild(grid); |
| } |
| |
| // التمرير اللانهائي |
| window.addEventListener('scroll', () => { |
| if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) { |
| if (currentQuery && !loading) { |
| loadResults(); |
| } |
| } |
| }); |
| |
| // تنفيذ البحث التلقائي إذا كان هناك استعلام في URL |
| const urlParams = new URLSearchParams(window.location.search); |
| const query = urlParams.get('q'); |
| if (query) { |
| document.getElementById('searchInput').value = query; |
| performSearch(); |
| } |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| LIVE_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>البث المباشر - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .live-badge { |
| background: #ff2d55; |
| animation: pulse 2s infinite; |
| } |
| @keyframes pulse { |
| 0% { opacity: 1; } |
| 50% { opacity: 0.7; } |
| 100% { opacity: 1; } |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white p-4"> |
| <div class="max-w-6xl mx-auto"> |
| <a href="/" class="inline-block glass-card px-4 py-2 rounded-2xl mb-6 hover:bg-[#ff2d55] transition"> |
| ← العودة للرئيسية |
| </a> |
| |
| <div class="flex justify-between items-center mb-8"> |
| <h1 class="text-3xl font-bold gradient-text">البث المباشر</h1> |
| <button onclick="startLive()" class="btn-primary px-8 py-3 rounded-2xl font-bold flex items-center gap-2"> |
| <span class="text-2xl">🎥</span> |
| بدء بث مباشر |
| </button> |
| </div> |
| |
| <div id="streams" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6"></div> |
| </div> |
| |
| <script> |
| function loadStreams() { |
| fetch('/api/live/active') |
| .then(res => res.json()) |
| .then(streams => { |
| const container = document.getElementById('streams'); |
| |
| if (streams.length === 0) { |
| container.innerHTML = ` |
| <div class="col-span-full text-center py-12"> |
| <div class="text-6xl mb-4">📡</div> |
| <p class="text-gray-400">لا توجد بثوث مباشرة حالياً</p> |
| </div> |
| `; |
| return; |
| } |
| |
| container.innerHTML = streams.map(s => ` |
| <div class="glass-card rounded-3xl overflow-hidden cursor-pointer hover:border-[#ff2d55] transition" onclick="window.location.href='/live/${s.id}'"> |
| <div class="relative aspect-video bg-gradient-to-br from-[#ff2d55] to-[#ff8a5c]"> |
| <div class="absolute top-2 right-2 live-badge text-white px-3 py-1 rounded-full text-sm font-bold flex items-center gap-1"> |
| <span class="w-2 h-2 bg-white rounded-full"></span> |
| مباشر |
| </div> |
| <div class="absolute bottom-2 left-2 bg-black/60 px-2 py-1 rounded-full text-sm flex items-center gap-1"> |
| <span>👁️</span> |
| <span>${s.viewers}</span> |
| </div> |
| </div> |
| <div class="p-4"> |
| <div class="flex items-center gap-3 mb-2"> |
| <img src="/avatars/${s.avatar}" class="w-10 h-10 rounded-full border-2 border-[#ff2d55]"> |
| <div> |
| <div class="font-bold">@${s.username}</div> |
| <div class="text-sm text-gray-400">${s.title || 'بث مباشر'}</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| `).join(''); |
| }); |
| } |
| |
| function startLive() { |
| const title = prompt('أدخل عنوان البث:'); |
| if (!title) return; |
| |
| fetch('/api/live/start', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({title}) |
| }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.status === 'ok') { |
| alert(`تم بدء البث!\n\nمفتاح البث: ${data.stream_key}\nخادم RTMP: ${data.rtmp_url}\n\nاستخدم هذه المعلومات في برنامج البث مثل OBS`); |
| loadStreams(); |
| } |
| }) |
| .catch(err => { |
| alert('فشل بدء البث. قد تحتاج عضوية VIP لهذه الميزة.'); |
| }); |
| } |
| |
| loadStreams(); |
| setInterval(loadStreams, 10000); // تحديث كل 10 ثوان |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| LEADERBOARD_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>المتصدرين - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| .gold { color: #FFD700; } |
| .silver { color: #C0C0C0; } |
| .bronze { color: #CD7F32; } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white p-4"> |
| <div class="max-w-4xl mx-auto"> |
| <a href="/" class="inline-block glass-card px-4 py-2 rounded-2xl mb-6 hover:bg-[#ff2d55] transition"> |
| ← العودة للرئيسية |
| </a> |
| |
| <div class="glass-card rounded-3xl p-8"> |
| <h1 class="text-3xl font-bold mb-8 text-center gradient-text">🏆 المتصدرين</h1> |
| |
| <div class="flex gap-4 mb-8 justify-center"> |
| <button onclick="loadLeaderboard('all')" class="px-6 py-2 rounded-2xl bg-[#ff2d55] text-white font-bold" id="btnAll">شامل</button> |
| <button onclick="loadLeaderboard('weekly')" class="px-6 py-2 rounded-2xl bg-white/5 hover:bg-[#ff2d55] transition" id="btnWeekly">أسبوعي</button> |
| <button onclick="loadLeaderboard('daily')" class="px-6 py-2 rounded-2xl bg-white/5 hover:bg-[#ff2d55] transition" id="btnDaily">يومي</button> |
| </div> |
| |
| <div id="leaderboard" class="space-y-3"></div> |
| </div> |
| </div> |
| |
| <script> |
| function loadLeaderboard(period) { |
| // تحديث الأزرار |
| ['all', 'weekly', 'daily'].forEach(p => { |
| const btn = document.getElementById(`btn${p.charAt(0).toUpperCase() + p.slice(1)}`); |
| if (p === period) { |
| btn.classList.add('bg-[#ff2d55]'); |
| btn.classList.remove('bg-white/5'); |
| } else { |
| btn.classList.remove('bg-[#ff2d55]'); |
| btn.classList.add('bg-white/5'); |
| } |
| }); |
| |
| fetch(`/api/gamification/leaderboard?period=${period}`) |
| .then(res => res.json()) |
| .then(data => { |
| const container = document.getElementById('leaderboard'); |
| |
| if (data.leaderboard.length === 0) { |
| container.innerHTML = '<p class="text-center text-gray-400 py-8">لا توجد بيانات</p>'; |
| return; |
| } |
| |
| let html = ''; |
| data.leaderboard.forEach((user, index) => { |
| let medal = ''; |
| if (index === 0) medal = '<span class="gold text-2xl">🥇</span>'; |
| else if (index === 1) medal = '<span class="silver text-2xl">🥈</span>'; |
| else if (index === 2) medal = '<span class="bronze text-2xl">🥉</span>'; |
| |
| html += ` |
| <div class="flex items-center gap-4 p-4 glass-card rounded-2xl ${index === 0 ? 'border border-[#ff2d55]' : ''}"> |
| <div class="w-8 text-center font-bold text-xl">${medal || (index + 1)}</div> |
| <img src="${user.avatar_url}" class="w-12 h-12 rounded-full border-2 border-[#ff2d55]"> |
| <div class="flex-1"> |
| <div class="flex items-center gap-2"> |
| <span class="font-bold">@${user.username}</span> |
| ${user.is_verified ? '<span class="text-[#ff2d55]">✓</span>' : ''} |
| </div> |
| <div class="text-sm text-gray-400">المستوى ${user.level || 1}</div> |
| </div> |
| <div class="text-left"> |
| <div class="font-bold text-[#ff2d55]">${user.xp || user.daily_xp || user.weekly_xp || 0}</div> |
| <div class="text-xs text-gray-500">XP</div> |
| </div> |
| </div> |
| `; |
| }); |
| |
| if (data.user_rank) { |
| html += ` |
| <div class="mt-6 pt-4 border-t border-gray-800"> |
| <p class="text-center text-gray-400">ترتيبك الحالي: <span class="text-[#ff2d55] font-bold">#${data.user_rank}</span></p> |
| </div> |
| `; |
| } |
| |
| container.innerHTML = html; |
| }); |
| } |
| |
| // تحميل المتصدرين الشامل افتراضياً |
| loadLeaderboard('all'); |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| GIFTS_TEMPLATE = ''' |
| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>الهدايا - ARC Video</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap'); |
| * { font-family: 'Tajawal', sans-serif; } |
| .glass-card { |
| background: rgba(20, 20, 30, 0.7); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 45, 85, 0.2); |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0a0a0a] min-h-screen text-white p-4"> |
| <div class="max-w-4xl mx-auto"> |
| <a href="/" class="inline-block glass-card px-4 py-2 rounded-2xl mb-6 hover:bg-[#ff2d55] transition"> |
| ← العودة للرئيسية |
| </a> |
| |
| <div class="glass-card rounded-3xl p-8"> |
| <h1 class="text-3xl font-bold mb-8 text-center gradient-text">🎁 الهدايا الافتراضية</h1> |
| |
| <div id="balance" class="text-center mb-8"> |
| <p class="text-gray-400">رصيدك الحالي</p> |
| <div class="flex justify-center gap-4 mt-2"> |
| <span class="text-2xl">🪙 <span id="coins">0</span></span> |
| <span class="text-2xl">💎 <span id="diamonds">0</span></span> |
| </div> |
| </div> |
| |
| <div id="gifts" class="grid md:grid-cols-2 lg:grid-cols-3 gap-4"></div> |
| </div> |
| </div> |
| |
| <script> |
| function loadBalance() { |
| fetch('/api/coins/balance') |
| .then(res => res.json()) |
| .then(data => { |
| document.getElementById('coins').textContent = data.coins; |
| document.getElementById('diamonds').textContent = data.diamonds; |
| }); |
| } |
| |
| function loadGifts() { |
| fetch('/api/gifts') |
| .then(res => res.json()) |
| .then(gifts => { |
| const container = document.getElementById('gifts'); |
| |
| container.innerHTML = gifts.map(g => ` |
| <div class="glass-card rounded-2xl p-6 text-center hover:border-[#ff2d55] transition"> |
| <div class="text-6xl mb-4">${g.animation_url ? '🎁' : '🎁'}</div> |
| <h3 class="font-bold text-lg mb-2">${g.name}</h3> |
| <p class="text-[#ff2d55] font-bold mb-4">🪙 ${g.price}</p> |
| <button onclick="buyGift(${g.id}, ${g.price})" class="w-full btn-primary px-4 py-2 rounded-2xl text-white"> |
| إرسال |
| </button> |
| </div> |
| `).join(''); |
| }); |
| } |
| |
| function buyGift(giftId, price) { |
| if (!confirm(`تأكيد شراء الهدية بقيمة ${price} عملة؟`)) return; |
| |
| fetch('/api/live/gift/1', { // سيتم تحديد stream_id لاحقاً |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({gift_id: giftId}) |
| }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.status === 'ok') { |
| alert('تم إرسال الهدية بنجاح'); |
| loadBalance(); |
| } else { |
| alert(data.error || 'فشل إرسال الهدية'); |
| } |
| }); |
| } |
| |
| loadBalance(); |
| loadGifts(); |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
| |
| def upload_video_to_cloudinary(file_path, public_id=None): |
| """رفع فيديو إلى Cloudinary مع تحسينات""" |
| 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": 600, "height": 600, "crop": "pad", "audio_codec": "none"}, |
| {"width": 1080, "height": 1920, "crop": "pad", "quality": "auto"} |
| ], |
| eager_async=True, |
| chunk_size=6000000, |
| timeout=120 |
| ) |
| return response.get('secure_url'), response.get('public_id') |
| except Exception as e: |
| logger.error(f"خطأ في رفع Cloudinary: {e}") |
| return None, None |
|
|
| def generate_video_thumbnail(video_public_id): |
| """توليد رابط الصورة المصغرة من Cloudinary""" |
| return cloudinary.CloudinaryVideo(video_public_id).video_thumbnail_url( |
| width=400, height=400, crop="fill", quality="auto" |
| ) |
|
|
| def add_watermark_to_cloudinary_video(public_id, text="ARC"): |
| """إنشاء رابط فيديو مع علامة مائية نصية""" |
| transformed_url = cloudinary.CloudinaryVideo(public_id).video_url( |
| transformation=[ |
| {"width": 1080, "height": 1920, "crop": "limit"}, |
| {"overlay": {"font_family": "Arial", "font_size": 60, "text": text}, |
| "gravity": "south_east", "opacity": 50, "x": 20, "y": 20} |
| ] |
| ) |
| return transformed_url |
|
|
| |
| def encrypt_video_file(input_path, output_path=None, key=None): |
| """تشفير ملف فيديو باستخدام AES-256-GCM""" |
| if not HAS_CRYPTO: |
| return None, None |
| |
| if output_path is None: |
| filename = os.path.basename(input_path) |
| output_path = os.path.join(ENCRYPTED_FOLDER, f"{filename}.enc") |
| |
| if key is None: |
| key = get_random_bytes(32) |
| |
| try: |
| with open(input_path, 'rb') as f: |
| data = f.read() |
| |
| cipher = AES.new(key, AES.MODE_GCM) |
| ciphertext, tag = cipher.encrypt_and_digest(data) |
| |
| |
| with open(output_path, 'wb') as f: |
| f.write(cipher.nonce) |
| f.write(tag) |
| f.write(ciphertext) |
| |
| return output_path, key |
| |
| except Exception as e: |
| logger.error(f"خطأ في التشفير: {e}") |
| return None, None |
|
|
| def decrypt_video_file(encrypted_path, key, output_path=None): |
| """فك تشفير فيديو""" |
| if not HAS_CRYPTO: |
| return None |
| |
| if output_path is None: |
| output_path = encrypted_path.replace('.enc', '') |
| |
| try: |
| with open(encrypted_path, 'rb') as f: |
| nonce = f.read(16) |
| tag = f.read(16) |
| ciphertext = f.read() |
| |
| cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) |
| data = cipher.decrypt_and_verify(ciphertext, tag) |
| |
| with open(output_path, 'wb') as f: |
| f.write(data) |
| |
| return output_path |
| |
| except Exception as e: |
| logger.error(f"خطأ في فك التشفير: {e}") |
| return None |
|
|
| def add_watermark_to_video(input_path, output_path=None, watermark_text="ARC"): |
| """إضافة علامة مائية نصية على الفيديو""" |
| if not HAS_CV2 or not HAS_PIL: |
| return None |
| |
| if output_path is None: |
| filename = os.path.basename(input_path) |
| output_path = os.path.join(WATERMARK_FOLDER, f"watermarked_{filename}") |
| |
| try: |
| cap = cv2.VideoCapture(input_path) |
| if not cap.isOpened(): |
| return None |
| |
| fps = int(cap.get(cv2.CAP_PROP_FPS)) |
| width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) |
| height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| |
| fourcc = cv2.VideoWriter_fourcc(*'mp4v') |
| out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) |
| |
| font = cv2.FONT_HERSHEY_SIMPLEX |
| text_size = cv2.getTextSize(watermark_text, font, 1, 2)[0] |
| text_x = width - text_size[0] - 20 |
| text_y = height - 20 |
| |
| frame_count = 0 |
| while True: |
| ret, frame = cap.read() |
| if not ret: |
| break |
| |
| |
| overlay = frame.copy() |
| cv2.putText(overlay, watermark_text, (text_x, text_y), font, 1, (255, 255, 255), 2, cv2.LINE_AA) |
| cv2.putText(overlay, f"ARC {datetime.now().year}", (20, 40), font, 0.7, (255, 45, 85), 2) |
| |
| |
| cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame) |
| |
| out.write(frame) |
| frame_count += 1 |
| |
| if frame_count % 100 == 0: |
| logger.info(f"تقدم العلامة المائية: {frame_count}/{total_frames}") |
| |
| cap.release() |
| out.release() |
| cv2.destroyAllWindows() |
| |
| return output_path |
| |
| except Exception as e: |
| logger.error(f"خطأ في إضافة العلامة المائية: {e}") |
| return None |
|
|
| 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() |
| |
| |
| frames = [] |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| sample_indices = [int(i * total_frames / 5) for i in range(5)] |
| |
| for idx in sample_indices: |
| cap.set(cv2.CAP_PROP_POS_FRAMES, idx) |
| ret, frame = cap.read() |
| if ret: |
| |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) |
| resized = cv2.resize(gray, (32, 32)) |
| frames.append(resized.flatten()) |
| |
| cap.release() |
| |
| if not frames: |
| return random_vector() |
| |
| |
| features = np.mean(frames, axis=0).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"خطأ في استخراج الخصائص: {e}") |
| return random_vector() |
|
|
| |
| if __name__ == '__main__': |
| with app.app_context(): |
| try: |
| init_db() |
| logger.info("✅ تم تهيئة قاعدة البيانات بنجاح") |
| print("✅ ARC Video Global - النسخة المتقدمة جاهزة للتشغيل") |
| print("=" * 60) |
| print("🚀 منصة ARC Video - إمبراطورية الفيديو العالمية") |
| print("📱 Glassmorphism UI - نظام توصيات ذكي - أمان متقدم") |
| print("💎 عضوية VIP - هدايا افتراضية - بث مباشر") |
| print("=" * 60) |
| except Exception as e: |
| logger.error(f"❌ خطأ أثناء التهيئة: {e}") |
| print(f"❌ خطأ: {e}") |
| |
| app.run(host='0.0.0.0', port=7860, debug=True, threaded=True) |