from flask_socketio import SocketIO
from flask import flash
import os
import json
from passlib.context import CryptContext
from PIL import Image
from flask import Flask, render_template, url_for, redirect, request, send_from_directory, flash, session
from flask_login import LoginManager, login_user, current_user, logout_user, UserMixin
from flask_restful import Api
from data.user_sid import UserSID
from sqlalchemy.exc import IntegrityError
from flask_sqlalchemy import SQLAlchemy
from data import db_session
from data.videos import Video
from data.users import User
from sqlalchemy.orm import joinedload
from data.bugs import Bug, BugComment, BugScreenshot
from data.login_form import LoginForm
from data.tracks import Track
from data.likes import Like
from data.albums import Album
from data.register_form import RegisterForm
from data.video_form import VideoForm
from data.users_resource import UsersResource, UserListResource
from data.videos_resource import VideosResource, VideoListResource
from data.likes_resource import LikeResource, NotLikeResource
from data.subscriptions_resource import FollowResource, NotFollowResource
from werkzeug.utils import secure_filename
from data.avatar_form import AvatarForm
from data.db_session import SqlAlchemyBase
from data.comments import Comment
from data.comment_form import CommentForm
from data.music import Music
from werkzeug.utils import secure_filename
from sqlalchemy import ForeignKey
from wtforms_components import ColorField
from sqlalchemy.orm import relationship
from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect
from sqlalchemy import func
from flask_migrate import Migrate
import uuid
import time
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime, timedelta
# from datetime import datetime
import pytz
from flask.json import JSONEncoder
from datetime import datetime, timedelta
import math

class CustomJSONEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            # Adjust the datetime by +3 hours
            adjusted_time = obj + timedelta(hours=3)
            # Serialize to ISO 8601 format or any preferred format
            return adjusted_time.isoformat()

        # If you have other custom types, handle them here

        # For all other types, use the default serialization
        return super().default(obj)

moscow_tz = pytz.timezone('Europe/Moscow')
local_time = datetime.now(moscow_tz)
print(local_time)
app = Flask(__name__)
app.config['SECRET_KEY'] = 'yandexlyceum_secret_key'

api = Api(app)
socketio = SocketIO(app)
login_manager = LoginManager()
login_manager.init_app(app)

pwd_context = CryptContext(
    schemes=["pbkdf2_sha256"],
    default="pbkdf2_sha256",
    pbkdf2_sha256__default_rounds=30000
)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../instance/music.db'

db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Set the folder to store avatars
AVATAR_UPLOAD_FOLDER = os.path.join(app.root_path, 'static', 'avatars')
# Allowed extensions
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
BUG_SCREENSHOT_FOLDER = os.path.join(app.root_path, 'static', 'bug_screenshots')
ALLOWED_EXTENSIONS_BUG = {'png', 'jpg', 'jpeg', 'gif'}

os.makedirs(BUG_SCREENSHOT_FOLDER, exist_ok=True)

app.config['BUG_SCREENSHOT_FOLDER'] = BUG_SCREENSHOT_FOLDER
app.config['AVATAR_UPLOAD_FOLDER'] = AVATAR_UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 300 * 1024 * 1024  # 2MB max upload sizes
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['COVER_FOLDER'] = 'static/covers'
app.config['ALLOWED_AUDIO_EXTENSIONS'] = {'mp3', 'wav', 'flac', 'ogg'}
app.config['ALLOWED_IMAGE_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}

# Создаем папки, если их нет
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['COVER_FOLDER'], exist_ok=True)
import tkinter as tk
import psutil
import time
import threading

# Модель для треков
from functools import wraps
from flask import abort
import random
from flask import send_file
import magic
import subprocess
from werkzeug.utils import secure_filename

UPLOAD_FOLDERA = 'uploadsvids'
PROCESSED_FOLDERA = 'processedvids'
ALLOWED_EXTENSIONS = {'mp4', 'mkv', 'avi', 'mov', 'flv'}

app.config['UPLOAD_FOLDERA'] = UPLOAD_FOLDERA
app.config['PROCESSED_FOLDERA'] = PROCESSED_FOLDERA

# Ensure that directories exist
os.makedirs(UPLOAD_FOLDERA, exist_ok=True)
os.makedirs(PROCESSED_FOLDERA, exist_ok=True)

# Define allowed extensions
# Define allowed file extensions as a global variable
ALLOWED_EXTENSIONS = {'mp4', 'mkv', 'avi', 'mov', 'flv'}
from mutagen import File as MutagenFile
app.json_encoder = CustomJSONEncoder
CLIENTS_DIR = 'data/clients'
CLIENTS_FILE = os.path.join(CLIENTS_DIR, 'clients.txt')

BLOCKED_IPS_DIR = 'data/blocked'
BLOCKED_IPS_FILE = os.path.join(BLOCKED_IPS_DIR, 'blocked_ips.txt')

# Create directories if they don't exist
os.makedirs(os.path.dirname(CLIENTS_FILE), exist_ok=True)
os.makedirs(os.path.dirname(BLOCKED_IPS_FILE), exist_ok=True)

# Ensure files exist
for file in [CLIENTS_FILE, BLOCKED_IPS_FILE]:
    if not os.path.exists(file):
        with open(file, 'w') as f:
            pass  # Create empty file

# Load blocked IPs into a set for quick access
blocked_ips = set()
def load_blocked_ips():
    global blocked_ips
    blocked_ips = set()
    try:
        with open(BLOCKED_IPS_FILE, 'r') as f:
            for line in f:
                ip = line.strip()
                if ip:
                    blocked_ips.add(ip)
    except FileNotFoundError:
        pass

load_blocked_ips()
@app.context_processor
def inject_helpers():
    return dict(get_client_ip=get_client_ip)
def get_client_ip():
    if request.headers.get('X-Forwarded-For'):
        # If behind a proxy
        ip = request.headers.get('X-Forwarded-For').split(',')[0].strip()
    else:
        ip = request.remote_addr
    return ip
# Helper functions to manage IP blocking
def block_ip(ip):
    global blocked_ips
    if ip not in blocked_ips:
        blocked_ips.add(ip)
        with open(BLOCKED_IPS_FILE, 'a') as f:
            f.write(f"{ip}\n")

def unblock_ip(ip):
    global blocked_ips
    if ip in blocked_ips:
        blocked_ips.remove(ip)
        with open(BLOCKED_IPS_FILE, 'w') as f:
            for b_ip in blocked_ips:
                f.write(f"{b_ip}\n")

def is_blocked(ip):
    return ip in blocked_ips

def get_client_ip():
    if request.headers.get('X-Forwarded-For'):
        # If behind a proxy
        ip = request.headers.get('X-Forwarded-For').split(',')[0].strip()
    else:
        ip = request.remote_addr
    return ip
from flask import g

# 1. before_request to check birthday
@app.before_request
def check_birthday_modal():
    g.show_birthday_modal = False  # Default flag

    if current_user.is_authenticated and current_user.birthday:
        # Adjust the current time to the desired timezone (e.g., Moscow)
        now = datetime.utcnow() + timedelta(hours=3)
        today = now.date()

        # Check if today is the user's birthday
        if (current_user.birthday.month, current_user.birthday.day) == (today.month, today.day):
            # Check if the modal has already been shown today
            if session.get('birthday_modal_shown_date') != today.isoformat():
                g.show_birthday_modal = True
                session['birthday_modal_shown_date'] = today.isoformat()

# 2. Context processor to inject the flag
@app.context_processor
def inject_birthday_modal():
    return {'show_birthday_modal': getattr(g, 'show_birthday_modal', False)}
# Logging and blocking logic before each request
@app.before_request
def log_and_block_ip():
    ip = get_client_ip()

    # Log the IP if not already logged
    with open(CLIENTS_FILE, 'a+') as f:
        f.seek(0)
        logged_ips = set(line.strip() for line in f)
        if ip not in logged_ips:
            f.write(f"{ip}\n")

    # Define admin routes to always allow access
    admin_routes = ['/admin/']

    # Define paths to exclude from blocking (e.g., '/blocked')
    exempt_paths = ['/blocked']

    # Allow access to admin routes and exempt paths
    if any(request.path.startswith(route) for route in admin_routes) or request.path in exempt_paths:
        return  # Allow admin and exempted routes access

    # Allow access to static files
    if request.path.startswith('/static/'):
        return

    # Check if the IP is blocked
    if is_blocked(ip):
        return redirect(url_for('blocked'))
import random

def get_random_videos(session, count=5):
    all_videos = session.query(Video).all()
    if len(all_videos) < count:
        return all_videos  # Return all if fewer than count available
    return random.sample(all_videos, count)  # Get random samples of the given count
# Обработчик подключения
@socketio.on('connect')
def handle_connect(*args, **kwargs):
    if current_user.is_authenticated:
        with db_sess.begin():  # Автоматически начнёт новую транзакцию
            user = db_sess.query(User).get(current_user.id)
            if user:
                sid = request.sid

                # Обновляем список SID
                user_sids = json.loads(user.sid_list)
                user_sids.append(sid)
                user.sid_list = json.dumps(user_sids)

                user.is_online = True
                db_sess.commit()
                emit('user_status', {'user_id': user.id, 'status': 'online'}, broadcast=True, include_self=False)
                print('включил')
    else:
        disconnect()

# Обработчик отключения
@socketio.on('disconnect')
def handle_disconnect():
    user = db_sess.query(User).get(current_user.id)
    sid = request.sid
    user_sid = db_sess.query(UserSID).filter_by(sid=sid).first()
    if user_sid:
        user = user_sid.user
        # Удаляем SID из списка
        user_sids = json.loads(user.sid_list)
        if sid in user_sids:
            user_sids.remove(sid)
            user.sid_list = json.dumps(user_sids)

        # Если больше нет активных SID, ставим is_online в False
        if not user_sids:
            user.is_online = False
            print('выключил')

        db_sess.commit()
        emit('user_status', {'user_id': user.id, 'status': 'offline'}, broadcast=True, include_self=False)
    else:
        print(f"SID {sid} not found in any user.")

# Route to display blocked page
@app.route('/blocked')
def blocked():
    return render_template('blocked.html')

# Decorator to restrict access to admin users
def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not getattr(current_user, 'is_admin', False):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

# Admin interface to manage client IPs
@app.route('/admin/clients', methods=['GET', 'POST'])
@admin_required
def admin_clients():
    # Read all logged client IPs
    try:
        with open(CLIENTS_FILE, 'r') as f:
            clients = {line.strip() for line in f}
    except FileNotFoundError:
        clients = set()

    # Prepare the list with block status
    client_list = []
    admin_ip = get_client_ip()  # Get admin's IP
    for ip in sorted(clients):
        client_list.append({
            'ip': ip,
            'blocked': is_blocked(ip)
        })

    if request.method == 'POST':
        action = request.form.get('action')
        ip = request.form.get('ip')
        if ip:
            if action == 'block':
                if ip != admin_ip:  # Prevent admin from blocking themselves
                    block_ip(ip)
                    flash(f"IP {ip} has been blocked.", 'success')
                else:
                    flash("You cannot block your own IP.", 'danger')
            elif action == 'unblock':
                unblock_ip(ip)
                flash(f"IP {ip} has been unblocked.", 'success')
            return redirect(url_for('admin_clients'))

    return render_template('admin_clients.html', clients=client_list, admin_ip=admin_ip)
def get_audio_info(filepath):
    try:
        audio = MutagenFile(filepath, easy=True)
        if audio is None:
            raise Exception("Unsupported file type or corrupted file.")

        audio_info = {
            "duration": int(audio.info.length),  # Duration in seconds
            "bitrate": audio.info.bitrate,  # Bitrate in bps
            "filetype": audio.mime[0] if audio.mime else "Unknown",
            "size": os.path.getsize(filepath),  # File size in bytes
            "metadata": dict(audio)  # Metadata tags
        }

        return audio_info

    except Exception as e:
        print(f"Error getting audio info: {e}")
        return None
from data.private_messages import PrivateMessage
from flask import jsonify
def should_redirect():
    try:
        with open('status.txt', 'r') as file:
            content = file.read().strip().lower()
            return content == 'yes'
    except FileNotFoundError:
        return False  # Handle the case where the file doesn't exist
import math
from datetime import datetime
def allowed_bug_screenshot(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS_BUG
def generate_shared_link(self):
        """Generates a unique shared link token."""
        self.shared_link = secrets.token_urlsafe(16)
def decline(number, one, two, five):
    """
    Возвращает правильную форму слова в зависимости от числа.

    Args:
        number (int): Число для склонения.
        one (str): Форма для числа 1 (например, "секунда").
        two (str): Форма для чисел 2-4 (например, "секунды").
        five (str): Форма для чисел 5 и более (например, "секунд").

    Returns:
        str: Правильная форма слова.
    """
    number = abs(number)
    last_two = number % 100
    last = number % 10

    if 11 <= last_two <= 14:
        return five
    if last == 1:
        return one
    if 2 <= last <= 4:
        return two
    return five


from flask import jsonify  # Ensure this import is present
@app.route('/admin_controls', methods=['GET', 'POST'])
def admin_controls():
    if not current_user.is_authenticated or current_user.id != 1:
        return forbidden('')  # Only allow user ID 1 (admin) to access this page

    if request.method == 'POST':
        # Adjust volume, bass, and treble based on form input
        volume = request.form.get('volume', type=float)
        bass = request.form.get('bass', type=float)
        treble = request.form.get('treble', type=float)

        # Emit an event to all connected clients
        socketio.emit('update_audio_settings', {
            'volume': volume,
            'bass': bass,
            'treble': treble
        }, namespace='/')  # Ensure the namespace matches your client-side setup

        # Return JSON response to the client
        return jsonify({'success': True, 'message': 'Audio settings updated successfully.'})

    return render_template('admin_controls.html')
@app.route('/bugs')
def bugs_list():
    bugs = db_sess.query(Bug).order_by(Bug.created_at.desc()).all()
    return render_template('bugs_list.html', bugs=bugs, current_user=current_user)
@app.route('/bugs/create', methods=['GET', 'POST'])
def create_bug():
    if request.method == 'POST':
        title = request.form.get('title')
        description = request.form.get('description')
        screenshots = request.files.getlist('screenshots')  # Получаем список файлов

        if not title or not description:
            flash('Название и описание обязательны.', 'danger')
            return redirect(request.url)

        new_bug = Bug(
            title=title,
            description=description,
            reporter_id=current_user.id
        )

        # Обработка нескольких скриншотов
        for screenshot in screenshots:
            if screenshot and allowed_bug_screenshot(screenshot.filename):
                filename = secure_filename(screenshot.filename)
                timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
                filename = f"{current_user.id}_{timestamp}_{filename}"
                screenshot.save(os.path.join(app.config['BUG_SCREENSHOT_FOLDER'], filename))
                bug_screenshot = BugScreenshot(filename=filename)
                new_bug.screenshots.append(bug_screenshot)

        db_sess.add(new_bug)
        db_sess.commit()
        flash('Баг успешно создан!', 'success')
        return redirect(url_for('bugs_list'))

    return render_template('create_bug.html', current_user=current_user)
@app.route('/bugs/<int:bug_id>', methods=['GET', 'POST'])
def bug_detail(bug_id):
    bug = db_sess.query(Bug).get(bug_id)
    if not bug:
        abort(404)

    if request.method == 'POST' and current_user.is_authenticated:
        content = request.form.get('comment')
        if content:
            new_comment = BugComment(
                bug_id=bug.id,
                user_id=current_user.id,
                content=content
            )
            db_sess.add(new_comment)
            db_sess.commit()
            flash('Комментарий добавлен.', 'success')
            return redirect(url_for('bug_detail', bug_id=bug.id))
        else:
            flash('Комментарий не может быть пустым.', 'danger')

    return render_template('bug_detail.html', bug=bug, current_user=current_user)
@app.route('/bugs/<int:bug_id>/close', methods=['POST'])
def close_bug(bug_id):
    bug = db_sess.query(Bug).get(bug_id)
    if not bug:
        abort(404)

    if current_user.id != 1 and not current_user.is_admin:
        abort(403)

    if bug.status == 'closed':
        flash('Баг уже закрыт.', 'info')
    else:
        bug.status = 'closed'
        db_sess.commit()
        flash('Баг закрыт.', 'success')

    return redirect(url_for('bug_detail', bug_id=bug.id))
@app.route('/bugs/<int:bug_id>/delete', methods=['POST'])
def delete_bug(bug_id):
    bug = db_sess.query(Bug).get(bug_id)
    if not bug:
        abort(404)

    if current_user.id != 1 and not current_user.is_admin:
        abort(403)

    # Удаление скриншота, если он существует
    if bug.screenshot:
        try:
            os.remove(os.path.join(app.config['BUG_SCREENSHOT_FOLDER'], bug.screenshot))
        except FileNotFoundError:
            pass

    db_sess.delete(bug)
    db_sess.commit()
    flash('Баг удален.', 'success')
    return redirect(url_for('bugs_list'))
def time_since(adjusted_upload_time):
    now = datetime.utcnow()
    diff = now - adjusted_upload_time
    seconds = diff.total_seconds()

    if seconds < 0:
        return "только что"  # If upload_time is in the future
    if seconds < 30:
        return "только что"
    elif seconds < 60:
        sec = math.floor(seconds)
        return f"{sec} {decline(sec, 'секунда', 'секунды', 'секунд')}"
    elif seconds < 3600:
        minutes = math.floor(seconds / 60)
        return f"{minutes} {decline(minutes, 'минута', 'минуты', 'минут')}"
    elif seconds < 86400:
        hours = math.floor(seconds / 3600)
        return f"{hours} {decline(hours, 'час', 'часа', 'часов')}"
    elif seconds < 604800:
        days = math.floor(seconds / 86400)
        return f"{days} {decline(days, 'день', 'дня', 'дней')}"
    elif seconds < 2592000:
        weeks = math.floor(seconds / 604800)
        return f"{weeks} {decline(weeks, 'неделя', 'недели', 'недель')}"
    elif seconds < 31536000:
        months = math.floor(seconds / 2592000)
        return f"{months} {decline(months, 'месяц', 'месяца', 'месяцев')}"
    elif seconds < 3153600000:
        years = math.floor(seconds / 31536000)
        return f"{years} {decline(years, 'год', 'года', 'лет')}"
    elif seconds < 3153600000000:
        centuries = math.floor(seconds / 3153600000)
        return f"{centuries} {decline(centuries, 'век', 'века', 'веков')}"
    else:
        millennia = math.floor(seconds / 3153600000000)
        return f"{millennia} {decline(millennia, 'тысячелетие', 'тысячелетия', 'тысячелетий')}"

# Jinja2 template filter
@app.template_filter('timesince')
def timesince_filter(upload_time):
    if not isinstance(upload_time, datetime):
        raise ValueError("upload_time должен быть объектом datetime")

    # The JSONEncoder already adjusts the time by +3 hours,
    # but if you need to use adjusted time elsewhere (like in templates),
    # you might need to adjust it here as well.
    adjusted_upload_time = upload_time + timedelta(hours=3)
    return time_since(adjusted_upload_time)

# This function will be executed before every request
@app.before_request
def check_file_and_redirect():
    # Exclude requests for static files and already redirected new_page
    if should_redirect() and not request.path.startswith('/static') and request.endpoint != 'pishu':
        return redirect(url_for('pishu'))
@app.route('/пишу код, сервер временно отключен.')

def pishu():
    params = {
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('underconstruct.html', **params)
@app.route('/video/<int:video_id>/share', methods=['POST'])
def share_video(video_id):
    db_sess = db_session.create_session()
    video = db_sess.query(Video).get(video_id)

    if video is None:
        flash('Видео не найдено.', 'danger')
        return redirect(url_for('index'))

    if video.author_id != current_user.id:
        flash('У вас нет прав для этого действия.', 'danger')
        return redirect(url_for('video', video_id=video_id))

    # Check if a shared link already exists
    if not video.shared_link:
        video.generate_shared_link()
        video.privacy = 2  # Set privacy to 'Shared' if not already
        try:
            db_sess.commit()
            flash('Ссылка для общего доступа создана.', 'success')
        except IntegrityError:
            db_sess.rollback()
            flash('Ошибка при создании ссылки. Попробуйте еще раз.', 'danger')
            return redirect(url_for('video', video_id=video_id))
    else:
        flash('Ссылка для общего доступа уже существует.', 'info')

    shared_url = url_for('video', video_id=video.id, shared=video.shared_link, _external=True)
    return jsonify({'shared_url': shared_url})  # Returning JSON for AJAX handling
@app.route('/faq')

def faq():
    params = {
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('underconstruct.html', **params)
@socketio.on('private_message')
def handle_private_message(data):
    """
    Handles sending of private messages.
    Expects data to contain:
    - 'receiver_id': ID of the user to send the message to
    - 'message': The message content
    """
    sender = current_user
    receiver_id = data.get('receiver_id')
    message_content = data.get('message')

    if not sender.is_authenticated:
        emit('error', {'message': 'Authentication required.'})
        return

    if not receiver_id or not message_content:
        emit('error', {'message': 'Invalid data.'})
        return

    receiver = db_sess.query(User).get(receiver_id)
    if not receiver:
        emit('error', {'message': 'Receiver not found.'})
        return

    # Save the message to the database
    private_message = PrivateMessage(
        sender_id=sender.id,
        receiver_id=receiver.id,
        content=message_content
    )
    db_sess.add(private_message)
    db_sess.commit()

    # Emit the message to the receiver's SIDs
    receiver_sids = [sid.sid for sid in receiver.sids]
    emit('receive_private_message', {
        'sender_id': sender.id,
        'sender_name': f"{sender.name} {sender.surname}",
        'message': message_content,
        'timestamp': private_message.timestamp.strftime('%Y-%m-%d %H:%M:%S')
    }, room=receiver_sids, namespace='/')

    # Optionally, emit the message back to the sender's other clients
    emit('receive_private_message', {
        'sender_id': sender.id,
        'sender_name': f"{sender.name} {sender.surname}",
        'message': message_content,
        'timestamp': private_message.timestamp.strftime('%Y-%m-%d %H:%M:%S')
    }, room=request.sid)

def convert_to_mp4(input_path, output_path):
    """
    Convert the input video file to MP4 format using FFmpeg.
    """
    try:
        subprocess.check_output([
            'ffmpeg', '-i', input_path, '-c:v', 'libx264',
            '-preset', 'fast', '-crf', '22',
            '-c:a', 'aac', '-b:a', '192k', output_path
        ])
    except subprocess.CalledProcessError as e:
        print(f"Failed to convert video. Error: {e.output}")
        raise
def allowed_file(filename):
    """
    Check if the file extension is in the allowed list.
    """
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# main2.py (continued)
# main2.py (continued)

@app.route('/private_chat')

def private_chat():
    users = db_sess.query(User).filter(User.id != current_user.id).all()
    params = {
        'title': 'Private Chat',
        'users': users,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('private_chat.html', **params)

@app.route('/api/chat_history/<int:receiver_id>')

def chat_history(receiver_id):
    sender_id = current_user.id
    receiver = db_sess.query(User).get(receiver_id)
    if not receiver:
        return jsonify({'success': False, 'message': 'Receiver not found.'}), 404

    messages = db_sess.query(PrivateMessage).filter(
        or_(
            (PrivateMessage.sender_id == sender_id) & (PrivateMessage.receiver_id == receiver_id),
            (PrivateMessage.sender_id == receiver_id) & (PrivateMessage.receiver_id == sender_id)
        )
    ).order_by(PrivateMessage.timestamp.asc()).all()

    messages_data = []
    for msg in messages:
        messages_data.append({
            'sender_id': msg.sender_id,
            'sender_name': f"{msg.sender.name} {msg.sender.surname}",
            'message': msg.content,
            'timestamp': msg.timestamp.strftime('%Y-%m-%d %H:%M:%S')
        })

    return jsonify({'success': True, 'messages': messages_data})
@app.route('/mp4', methods=['GET', 'POST'])
def upload_mp4():
    if request.method == 'POST':
        if 'file' not in request.files:
            return "No file part"

        file = request.files['file']

        if file.filename == '':
            return "No selected file"

        # Call the allowed_file function with the filename only
        if file:
            filename = secure_filename(file.filename)
            file_path = os.path.join(app.config['UPLOAD_FOLDERA'], filename)
            file.save(file_path)

            mime = magic.Magic(mime=True)
            mime_type = mime.from_file(file_path)

            output_path = os.path.join(app.config['PROCESSED_FOLDERA'], f'{os.path.splitext(filename)[0]}.mp4')
            if mime_type != 'video/mp4':
                convert_to_mp4(file_path, output_path)
            else:
                convert_to_mp4(file_path, output_path)

            return redirect(url_for('uploaded_mp4', filename=os.path.basename(output_path)))

    return render_template('mp4.html')
@app.route('/tools', methods=['GET', 'POST'])
def tools():
    params = {
        'db_sess': db_sess,
        'User': User,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('tools.html', **params)

@app.route('/mp4s/<filename>')
def uploaded_mp4(filename):
    return send_from_directory(app.config['PROCESSED_FOLDERA'], filename)


UPLOAD_FOLDERI = 'static/uploadsimg'
CONVERTED_FOLDER = 'static/convertedimg'
os.makedirs(UPLOAD_FOLDERI, exist_ok=True)
os.makedirs(CONVERTED_FOLDER, exist_ok=True)

app.config['UPLOAD_FOLDERI'] = UPLOAD_FOLDERI

@app.route('/img2jpg', methods=['GET', 'POST'])
def convert_image():
    converted_file_url = None

    if request.method == 'POST':
        if 'image' not in request.files:
            return 'No file part', 400
        file = request.files['image']

        if file.filename == '':
            return 'No selected file', 400

        # Save the original file
        filename = secure_filename(file.filename)
        upload_path = os.path.join(app.config['UPLOAD_FOLDERI'], filename)
        file.save(upload_path)

        # Convert to JPG
        base_filename = os.path.splitext(filename)[0]
        jpg_filename = f"{base_filename}.jpg"
        converted_path = os.path.join(CONVERTED_FOLDER, jpg_filename)

        with Image.open(upload_path) as img:
            rgb_im = img.convert('RGB')  # Convert to RGB
            rgb_im.save(converted_path, 'JPEG')

        # Remove the original uploaded file
        os.remove(upload_path)

        # Get URL for downloading
        converted_file_url = url_for('static', filename=f'convertedimg/{jpg_filename}')

    return render_template('convert.html', converted_file_url=converted_file_url)
def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not current_user.is_admin:
            abort(403)  # Запрещено
        return f(*args, **kwargs)
    return decorated_function

def owner_or_admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        track_id = kwargs.get('track_id')
        track = Track.query.get_or_404(track_id)
        if not current_user.is_authenticated:
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

@app.route('/delete_track/<int:track_id>', methods=['POST'])
def delete_track(track_id):
    track = db.session.query(Track).get(track_id)
    if not track:
        abort(404)  # Handle case where the track is not found

    try:
        # Delete audio file if it exists
        audio_path = os.path.join(app.config['UPLOAD_FOLDER'], track.audio_filename)
        if os.path.exists(audio_path):
            os.remove(audio_path)

        # List of default cover filenames
        default_covers = ['default.png']

        # Delete track cover if it's not a default cover
        if track.cover_filename and track.cover_filename not in default_covers:
            cover_path = os.path.join(app.config['COVER_FOLDER'], track.cover_filename)
            if os.path.exists(cover_path):
                os.remove(cover_path)

        # Delete the track record from the database
        db.session.delete(track)
        db.session.commit()

        flash('Трек успешно удалён.', 'success')
    except Exception as e:
        flash(f'Ошибка при удалении трека: {e}', 'danger')
        db.session.rollback()  # Rollback the session in case of error

    return redirect(url_for('tracks'))






# Проверка разрешенных расширений файлов
def allowed_file(filename, allowed_extensions):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in allowed_extensions

from sqlalchemy import func

@app.route('/audiosearch')
def audiosearch():
    query = request.args.get('query', '').strip()

    results = []
    if query:
        search_pattern = f"%{query.lower()}%"

        # Query to find matches in Track titles
        results = db.session.query(Track).filter(
            func.lower(Track.title).like(search_pattern)
        ).all()

    return render_template('search_results2.html', query=query, tracks=results)
# Генерация уникального идентификатора сессии
@app.before_request
def make_session_permanent():
    if 'session_id' not in session:
        session['session_id'] = str(uuid.uuid4())

# Страница загрузки трека
@app.route('/upload_audio', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        # Get form data
        title = request.form.get('title')
        audio_file = request.files.get('audio_file')
        cover_file = request.files.get('cover_file')
        album_id = request.form.get('album_id')
        new_album_title = request.form.get('new_album_title')
        new_album_artist = request.form.get('new_album_artist')
        new_album_cover = request.files.get('new_album_cover')

        # Check if form data contains album selection
        if album_id:
            album = db.session.query(Album).get(album_id)
            if not album:
                flash('Выбранный альбом не существует.', 'danger')
                return redirect(url_for('upload'))
        else:
            album_id = None

        # Validate required fields
        if not title or not audio_file:
            flash('Название и аудио файл обязательны для заполнения.', 'danger')
            return redirect(url_for('upload'))

        # Validate audio file type
        if not allowed_file(audio_file.filename, app.config['ALLOWED_AUDIO_EXTENSIONS']):
            flash('Неверный тип аудио файла.', 'danger')
            return redirect(url_for('upload'))

        # Handle new album creation
        if new_album_title and new_album_artist:
            # Handle new album cover
            new_album_cover_filename = 'default_album.png'  # Default if no cover is provided
            if new_album_cover and allowed_file(new_album_cover.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']):
                new_album_cover_filename = f"{uuid.uuid4()}_{secure_filename(new_album_cover.filename)}"
                new_album_cover_path = os.path.join(app.config['COVER_FOLDER'], new_album_cover_filename)
                new_album_cover.save(new_album_cover_path)

            # Create new album record
            new_album = Album(title=new_album_title, artist=new_album_artist, cover_filename=new_album_cover_filename)
            db.session.add(new_album)
            db.session.commit()
            album_id = new_album.id
            flash('Новый альбом успешно создан!', 'success')

        # Save audio file
        audio_filename = f"{uuid.uuid4()}_{secure_filename(audio_file.filename)}"
        audio_path = os.path.join(app.config['UPLOAD_FOLDER'], audio_filename)
        audio_file.save(audio_path)

        # Save or default track cover
        track_cover_filename = 'default.png'
        if cover_file and allowed_file(cover_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']):
            track_cover_filename = f"{uuid.uuid4()}_{secure_filename(cover_file.filename)}"
            cover_path = os.path.join(app.config['COVER_FOLDER'], track_cover_filename)
            cover_file.save(cover_path)

        # Create new track record
        new_track = Track(
            title=title,
            album_id=album_id,
            cover_filename=track_cover_filename,
            audio_filename=audio_filename,
            owner_id=current_user.id
        )

        db.session.add(new_track)
        db.session.commit()
        flash('Трек успешно загружен!', 'success')
        return redirect(url_for('track', track_id=new_track.id))

    # Retrieve all albums for the dropdown
    albums = db.session.query(Album).all()
    return render_template('upload.html', albums=albums)


# Маршрут для отображения всех альбомов
@app.route('/albums')
def albums():
    # Querying all albums ordered by upload_date in descending order
    all_albums = db.session.query(Album).order_by(Album.upload_date.desc()).all()
    all_tracks = db.session.query(Track).all()
    # Pass the queried album list to the template
    return render_template('albums.html', albums=all_albums, tracks=all_tracks)

# Маршрут для создания нового альбома
@app.route('/create_album', methods=['GET', 'POST'])
def create_album():
    # Только авторизованные пользователи могут создавать альбомы

    if request.method == 'POST':
        title = request.form.get('title')
        artist = request.form.get('artist')
        cover_file = request.files.get('cover_file')

        if not title or not artist:
            flash('Название и исполнитель обязательны для заполнения.', 'danger')
            return redirect(url_for('create_album'))

        # Обработка обложки альбома
        if cover_file and allowed_file(cover_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']):
            cover_filename = str(uuid.uuid4()) + '_' + secure_filename(cover_file.filename)
            cover_path = os.path.join(app.config['COVER_FOLDER'], cover_filename)
            cover_file.save(cover_path)
        else:
            cover_filename = 'default_album.png'  # Используйте стандартную обложку

        # Создание нового альбома
        new_album = Album(title=title, artist=artist, cover_filename=cover_filename)
        db.session.add(new_album)
        db.session.commit()
        flash('Альбом успешно создан!', 'success')
        return redirect(url_for('albums'))

    return render_template('create_album.html')

# Маршрут для просмотра отдельного альбома с его треками
@app.route('/album/<int:album_id>')
def view_album(album_id):
    # Get the album by ID, or return 404 if not found
    album = db.session.query(Album).get_or_404(album_id)

    # Get all tracks associated with the album
    tracks = db.session.query(Track).filter_by(album_id=album_id).all()

    # Prepare parameters for rendering the template
    params = {
        'title': f"Album: {album.title} by {album.artist}",
        'album': album,
        'tracks': tracks,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }

    # Render the album view with the album and its tracks
    return render_template('view_album.html', **params)
# Страница со списком всех треков
@app.route('/record')
def record_audio():
    params = {
        'title': f"Записать аудио",
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('record.html', **params)
@app.route('/tracks')
def tracks():
    all_albums = db.session.query(Album).all()
    all_tracks = db.session.query(Track).filter(Track.album_id == None).all()
    random_albums = random.sample(all_albums, 3)
    params = {
        'user': user,
        'all_albums': all_albums
    }
    # Define a list of colors for default.png covers
    color_map = ["#FFB6C1", "#FFD700", "#ADFF2F", "#87CEFA", "#FF69B4", "#98FB98", "#FF6347", "#8A2BE2"]

    return render_template('tracks.html', tracks=all_tracks, color_map=color_map, random_albums=random_albums, albums=all_albums)
# Page for individual track
@app.route('/snake')
def snake():
    return render_template('snake.html')
import openai
@app.route('/chatgpt', methods=['GET', 'POST'])
def chatgpt():
    if request.method == 'POST':
        api_key = request.form.get('api_key')
        if api_key:
            session['api_key'] = api_key
            return redirect(url_for('chatwithgpt'))
        else:
            flash('Please enter a valid OpenAI API Key.', 'danger')
    return render_template('chatgpt.html')

# Route for the chat interface
@app.route('/chatwithgpt', methods=['GET', 'POST'])
def chatwithgpt():
    if 'api_key' not in session:
        flash('Please enter your OpenAI API Key first.', 'warning')
        return redirect(url_for('chatgpt'))

    response = None
    if request.method == 'POST':
        user_input = request.form.get('user_input')
        if user_input:
            # Set the OpenAI API Key from the session
            openai.api_key = session['api_key']
            try:
                # Make a request to OpenAI's API (using ChatGPT model)
                completion = openai.ChatCompletion.create(
                    model="gpt-3.5-turbo",
                    messages=[
                        {"role": "system", "content": "You are a helpful assistant."},
                        {"role": "user", "content": user_input}
                    ]
                )
                response = completion.choices[0].message['content']
            except openai.error.AuthenticationError:
                flash('Authentication failed. Please check your API Key.', 'danger')
                return redirect(url_for('chatgpt'))
            except Exception as e:
                flash(f'An error occurred: {str(e)}', 'danger')
        else:
            flash('Please enter a message to send.', 'warning')

    return render_template('chat.html', response=response)
@app.route('/track/<int:track_id>')
def track(track_id):
    track = db.session.query(Track).get(track_id)
    if track is None:
        return "Track not found", 404

    # Path to the audio file
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], track.audio_filename)

    # Get audio information
    audio_info = get_audio_info(filepath)

    # Check if the user has liked this track
    session_id = session.get('session_id')
    liked = db.session.query(Like).filter_by(session_id=session_id, track_id=track_id).first() is not None

    return render_template('track.html', track=track, liked=liked, audio_info=audio_info)

# Обработка лайков
@app.route('/like_music/<int:track_id>', methods=['POST'])
def like(track_id):
    session_id = session.get('session_id')
    if session_id is None:
        return redirect(url_for('tracks'))

    # Use db.session.query to retrieve the track
    track = db.session.query(Track).get(track_id)
    if not track:
        return "Track not found", 404

    # Use db.session.query for checking Likes
    existing_like = db.session.query(Like).filter_by(session_id=session_id, track_id=track_id).first()

    # Create a new Like if none exists
    if not existing_like:
        new_like = Like(session_id=session_id, track_id=track_id)
        track.likes += 1
        db.session.add(new_like)
        db.session.commit()

    return redirect(url_for('track', track_id=track_id))

# Page with recommendations
@app.route('/recommendations')
def recommendations():
    # Query top liked tracks using the session-based query
    top_liked = db.session.query(Track).order_by(Track.likes.desc()).limit(5).all()

    # Query newest tracks ordered by upload date
    newest = db.session.query(Track).order_by(Track.upload_date.desc()).limit(5).all()

    return render_template('recommendations.html', top_liked=top_liked, newest=newest)

# Отдача обложек и аудио файлов
#@app.route('/covers/<filename>')
#def cover(filename):
 #   return send_from_directory(app.config['COVER_FOLDER'], filename)

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

@app.route('/favicon.ico')
def favicon():
    """Возвращает кастомную иконку страницы в браузере"""
    return send_from_directory(os.path.join(app.root_path, 'static/img'),
                               'vhs.ico', mimetype='image/vnd.microsoft.icon')


@socketio.on('comment')
def handle_comment_event(username, video_title):
    message = f"{username} commented on your video '{video_title}'"
    socketio.emit('notify', {'message': message})


def like_video(video_id):
    # Assuming this function is called during a like action
    video = db_sess.query(Video).get(video_id)
    if video:
        message = f"Someone liked your video '{video.title}'"
        socketio.emit('notify', {'message': message}, room=video.author_id)  # send to video author
def subscribe_to_user(user_id):
    # Assuming this function is called during a subscribe action
    user = db.session.query(User).first()
    if user:
        message = "Someone subscribed to you!"
        socketio.emit('notify', {'message': message}, room=user.id)


def load_user(user_id):
    return db_sess.get(User, user_id)

@app.route('/upload_avatar', methods=['GET', 'POST'])
def upload_avatar():
    """Allows authenticated users to upload and customize their avatar."""
    form = AvatarForm()
    if request.method == 'POST':
        # Handling form submission and image saving
        if 'avatar' in request.files:
            avatar = request.files['avatar']
            filename = secure_filename(avatar.filename)
            file_ext = os.path.splitext(filename)[1].lower()
            avatar_filename = f"user_{current_user.id}{file_ext}"
            avatar_path = os.path.join(app.config['AVATAR_UPLOAD_FOLDER'], avatar_filename)

            # Save and process the avatar
            try:
                with Image.open(avatar) as img:
                    img = img.convert('RGB')
                    img.thumbnail((300, 300))
                    img.save(avatar_path, optimize=True, quality=85)

                # Update user's avatar in the database
                current_user.avatar = avatar_filename
                db_sess.commit()
                flash("Аватар успешно обновлен!", "success")
                return redirect(url_for('user', user_id=current_user.id))

            except Exception as e:
                flash('Загрузка не удалась. Пожалуйста, попробуйте снова.', 'danger')
                return redirect(url_for('upload_avatar'))

    return render_template('upload_avatar.html', form=form)

import signal
from flask import current_app, abort
import psutil

# Function to get system load
def get_system_load():
    return {
        'cpu_percent': psutil.cpu_percent(interval=1),
        'memory_percent': psutil.virtual_memory().percent,
        'disk_usage_percent': psutil.disk_usage('/').percent,
    }

@app.route('/admin/control_panel')
def admin_control_panel():
    if not current_user.is_authenticated or not current_user.is_admin:
        return forbidden('')

    system_load = get_system_load()

    params = {
        'title': 'Server Control Panel',
        'system_load': system_load,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }

    return render_template('admin_control_panel.html', **params)

@app.route('/admin/stop_server')
def stop_server():
    if not current_user.is_authenticated or not current_user.is_admin:
        return forbidden('')

    os.kill(os.getpid(), signal.SIGTERM)  # Gracefully terminate the server
    return "Server is stopping."

@app.route('/admin/stop_server_delayed')
def stop_server_delayed():
    if not current_user.is_authenticated or not current_user.is_admin:
        return forbidden('')

    def delayed_shutdown():
        time.sleep(300)  # Delay for 5 minutes
        os.kill(os.getpid(), signal.SIGTERM)  # Then terminate the server

    threading.Thread(target=delayed_shutdown).start()  # Run in a separate thread
    return "Server will stop in 5 minutes."

@app.route('/admin/reload')
def reload_server():
    if not current_user.is_authenticated or not current_user.is_admin:
        return forbidden('')

    os.kill(os.getpid(), signal.SIGHUP)  # Reload the server
    return "Server is reloading."

@app.errorhandler(403)
def forbidden(e):
    """Возвращает кастомную страницу ошибки 403"""
    params = {
        'title': 'Доступ запрещен',
        'message': 'Похоже, у вас нет доступа к этой странице ¯\\_(ツ)_/¯',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('error1.html', **params), 403

def to_int(value):
    try:
        return int(value)
    except ValueError:
        return 0  # or handle as needed

app.jinja_env.filters['to_int'] = to_int

@app.errorhandler(404)
def page_not_found(e):
    """Возвращает кастомную страницу ошибки 404"""
    params = {
        'title': 'Страница не найдена',
        'message': 'Похоже, мы не можем найти нужную вам страницу ¯\\_(ツ)_/¯',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('error2.html', **params), 404


@app.errorhandler(405)
def method_not_allowed(e):
    """Возвращает кастомную страницу ошибки 405"""
    params = {
        'title': 'Метод не разрешен',
        'message': 'Похоже, этот метод не разрешен ¯\\_(ツ)_/¯',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('error3.html', **params), 405


@app.errorhandler(500)
def internal_server_error(e):
    """Возвращает кастомную страницу ошибки 500"""
    params = {
        'title': 'Ошибка сервера',
        'message': 'Похоже, на сервере произошла непредвиденная ошибка ¯\\_(ツ)_/¯',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('error4.html', **params), 500


# In your main application file (e.g., main2.py)

from sqlalchemy.orm import joinedload

@app.route('/')
@app.route('/index')
def index():
    """Returns the home page"""
    # Eagerly load the 'author' relationship to avoid additional queries in the template
    all_videos = db_sess.query(Video).options(joinedload(Video.author)).all()

    if len(all_videos) > 10:
        last_videos = all_videos[-10:][::-1]  # Get the last 10 videos
    else:
        last_videos = all_videos[::-1]  # Reverse the list

    best_videos = sorted(all_videos, key=lambda x: -x.likes)[:3]
    underrated_videos = [video for video in all_videos if video.likes == 0]

    if len(underrated_videos) > 9:
        underrated_videos = underrated_videos[-9:][::-1]
    else:
        underrated_videos = underrated_videos[::-1]

    params = {
        'title': 'Video Hosting Service',
        'last_videos': last_videos,
        'best_videos': best_videos,
        'underrated_videos': underrated_videos,
        'user': user,
        'db_sess': db_sess,
        'User': User,
        'Video': Video,
        'authenticated': current_user.is_authenticated,
        'all_videos': all_videos,
        'current_user': current_user
    }
    return render_template('index.html', **params)


@login_manager.user_loader
def load_user(user_id):
    """Возвращает пользователя"""
    return db_sess.query(User).get(user_id)


@app.route('/login', methods=['GET', 'POST'])
def login():
    """Возвращает страницу авторизации и авторизует пользователя"""
    if current_user.is_authenticated:
        return forbidden('')
    form = LoginForm()
    params = {
        'title': 'Авторизация',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    if form.validate_on_submit():
        user = db_sess.query(User).filter(User.login == form.login.data).first()
        if user and pwd_context.verify(form.password.data, user.password):
            login_user(user, remember=form.remember_me.data)
            return redirect('/')
        return render_template('login.html',
                               message='Неправильный логин или пароль',
                               form=form, **params)
    return render_template('login.html', form=form, **params)

from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user

@app.route('/edit_comment/<int:comment_id>', methods=['GET', 'POST'])
@login_required
def edit_comment(comment_id):
    # Fetch the comment from the database using comment_id
    comment = db_sess.query(Comment).filter(Comment.id == comment_id).first()

    # Ensure the comment exists and the current_user is the author
    if comment is None or comment.user_id != current_user.id:
        flash('Comment not found or you do not have permission to edit it.', 'danger')
        return redirect(url_for('video', video_id=comment.video_id))  # Redirect to the video's page

    if request.method == 'POST':
        new_content = request.form.get('content')
        if new_content:
            comment.content = new_content
            db_sess.commit()
            flash('Comment updated successfully!', 'success')
        else:
            flash('No content provided for update.', 'danger')
        return redirect(url_for('video', video_id=comment.video_id))

    return render_template('edit_comment.html', comment=comment)

@app.route('/register', methods=['GET', 'POST'])
def register():
    """Возвращает страницу регистрации и регистрирует пользователя"""
    if current_user.is_authenticated:
        return forbidden('')
    form = RegisterForm()
    params = {
        'title': 'Регистрация',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    if form.validate_on_submit():
        user = db_sess.query(User).filter(User.login == form.login.data).all()
        if user:
            return render_template('register.html',
                                   message='Пользователь с таким логином уже существует',
                                   form=form, **params)
        if form.password.data != form.repeat_password.data:
            return render_template('register.html',
                                   message='Пароли не совпадают',
                                   form=form, **params)
        user = User()
        user.login = form.login.data
        user.password = pwd_context.hash(form.password.data)
        user.name = form.name.data
        user.surname = form.surname.data
        db_sess.add(user)
        db_sess.commit()
        login_user(user, remember=form.remember_me.data)
        return redirect('/')
    return render_template('register.html', form=form, **params)
from flask import jsonify, request, current_app

@app.route('/api/like_comment/<int:comment_id>', methods=['POST']) # Ensure the user is logged in
def like_comment(comment_id):
    # Logic to handle liking a comment
    comment = db_sess.query(Comment).filter_by(id=comment_id).first()
    if not comment:
        return jsonify({'success': False, 'message': 'Comment not found.'}), 404

    # Example logic: increment the like count
    comment.likes += 1
    db_sess.commit()

    return jsonify({'success': True, 'new_like_count': comment.likes})

@app.route('/logout')
def logout():
    """
    Производит выход пользователя из системы, возвращает домашнюю страницу.
    Если пользователь не авторизован, возвращает страницу ошибки 403
    """
    if current_user.is_authenticated:
        logout_user()
        return redirect('/')
    else:
        return forbidden('')
@app.route('/api/pin_comment/<int:comment_id>', methods=['POST'])
def pin_comment(comment_id):
    comment = db_sess.query(Comment).get(comment_id)
    if not comment:
        return {'error': 'Comment not found'}, 404
    video = db_sess.query(Video).get(comment.video_id)

    # Сбрасываем все закрепленные комментарии для данного видео
    db_sess.query(Comment).filter(Comment.video_id == video.id).update({'pinned': False})
    # Закрепляем выбранный комментарий
    comment.pinned = True
    db_sess.commit()
    return {'success': True}
@app.route('/delete_comment/<int:comment_id>', methods=['POST'])
def delete_comment(comment_id):
    comment = db_sess.query(Comment).get(comment_id)
    if not comment:
        flash('Комментарий не найден.', 'danger')
        return redirect(request.referrer)
    if not current_user.is_authenticated or current_user.id != comment.user_id:
        flash('Вы не можете удалить этот комментарий.', 'danger')
        return redirect(request.referrer)
    db_sess.delete(comment)
    db_sess.commit()
    flash('Комментарий удален.', 'success')
    return redirect(request.referrer)
from flask import abort
from data.video_views import VideoView
@app.route('/video/<int:video_id>', methods=['GET', 'POST'])
def video(video_id):
    db_sess = db_session.create_session()
    current_video = db_sess.query(Video).get(video_id)
    if current_video is None:
        return page_not_found('')
    random_videos = get_random_videos(db_sess, count=12)

    # Access Control Logic
    shared = request.args.get('shared', type=str)
    # By default, access is denied
    access_granted = True

    if current_video.privacy == 1:  # Public
        access_granted = True
    elif current_video.privacy == 2:  # Shared
        if shared and shared == current_video.shared_link:
            access_granted = True
    elif current_video.privacy == 3:  # Private
        if current_user.is_authenticated and current_user.id == current_video.author_id:
            access_granted = True

    if not access_granted:
        flash('У вас нет доступа к этому видео.', 'danger')
        return redirect(url_for('index'))

    # Proceed with existing logic
    comments_enabled = current_video.comments_enabled
    author = current_video.author

    # Efficient retrieval of last 10 videos
    last_videos = db_sess.query(Video)\
        .order_by(Video.datetime.desc())\
        .limit(10).all()

    like = False
    subscription = False

    if current_user.is_authenticated:
        # Handle view counting
        try:
            new_view = VideoView(user_id=current_user.id, video_id=video_id)
            db_sess.add(new_view)
            current_video.views += 1
            db_sess.commit()
        except IntegrityError:
            db_sess.rollback()  # View already exists

        # Handle likes and subscriptions using association tables
        likes = json.loads(current_user.likes) if current_user.likes else []
        subscriptions = json.loads(current_user.subscriptions) if current_user.subscriptions else []
        like = video_id in likes
        subscription = current_video.author_id in subscriptions

    # Handle comment form
    form = CommentForm()
    if comments_enabled and form.validate_on_submit():
        if current_user.is_authenticated and not current_user.is_blocked:
            comment = Comment(
                content=form.content.data,
                user_id=current_user.id,
                video_id=video_id,
                parent_id=form.parent_id.data or None
            )
            db_sess.add(comment)
            db_sess.commit()
            flash('Комментарий добавлен!', 'success')
            return redirect(url_for('video', video_id=video_id))
        else:
            flash('Вы не можете оставить комментарий.', 'danger')
            return redirect(url_for('video', video_id=video_id))

    # Retrieve comments with eager loading
    comments = db_sess.query(Comment).filter(Comment.video_id == video_id).options(
        joinedload(Comment.user),
        joinedload(Comment.parent).joinedload(Comment.user),
        joinedload(Comment.replies).joinedload(Comment.user)
    ).order_by(Comment.datetime.desc()).all()

    # Build root comments
    comments_by_id = {comment.id: comment for comment in comments}
    root_comments = []
    for comment in comments:
        if comment.parent_id is None:
            root_comments.append(comment)
        else:
            parent = comments_by_id.get(comment.parent_id)
            if parent:
                if not hasattr(parent, 'replies_sorted'):
                    parent.replies_sorted = []
                parent.replies_sorted.append(comment)

    params = {
        'title': f"{current_video.title} - {author.name} {author.surname}",
        'current_video': current_video,
        'video_src': url_for("static", filename=f'vid/{video_id}.mp4'),
        'author': author,
        'like': like,
        'random_videos': random_videos,  # Pass random videos to the template
        'subscription': subscription,
        'comments': root_comments,  # Root comments only
        'last_videos': last_videos,
        'form': form,
        'avatar_url': url_for('static', filename=f'avatars/{author.avatar}'),
        'authenticated': current_user.is_authenticated,
        'current_user': current_user,
    }
    return render_template('video.html', **params)



@app.route('/add_video', methods=['GET', 'POST'])
def add_video():
    if not current_user.is_authenticated:
        return forbidden('')  # Ensure you have a `forbidden` handler defined

    form = VideoForm()
    params = {
        'title': 'Добавление видео',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }

    if current_user.is_blocked:
        flash('Ваш аккаунт заблокирован и вы не можете загружать видео.', 'danger')
        return render_template('add_video.html', form=form, **params)

    if form.validate_on_submit():
        video_file = form.video.data
        preview_file = form.preview.data
        datetime_input = form.datetime.data  # Получаем выбранную дату

        # Установка даты загрузки
        video_datetime = datetime_input if datetime_input else datetime.utcnow()

        # Validate video file format
        if not video_file.filename.lower().endswith('.mp4'):
            flash('Данный формат видео не поддерживается. Пожалуйста, загрузите видео в формате mp4.', 'danger')
            return render_template('add_video.html', form=form, **params)

        # Validate preview image format
        if not preview_file.filename.lower().endswith('.jpg'):
            flash('Данный формат обложки не поддерживается. Пожалуйста, загрузите изображение в формате jpg.', 'danger')
            return render_template('add_video.html', form=form, **params)

        try:
            # Create a new Video object
            video = Video(
                title=form.title.data,
                description='<br>'.join(form.description.data.split('\n')),
                author_id=current_user.id,
                comments_enabled=form.comments_enabled.data,
                datetime=video_datetime  # Assuming your Video model has this field
            )
            db_sess.add(video)
            db_sess.commit()

            # Save uploaded files
            video_path = os.path.join('static/vid', f'{video.id}.mp4')
            preview_path = os.path.join('static/pre', f'{video.id}.jpg')
            video_file.save(video_path)
            preview_file.save(preview_path)

            flash('Видео успешно загружено!', 'success')
            return redirect('/')

        except Exception as e:
            db_sess.rollback()
            flash(f'Произошла ошибка при загрузке видео: {e}', 'danger')
            return render_template('add_video.html', form=form, **params)

    return render_template('add_video.html', form=form, **params)


@app.route('/edit_video/<int:video_id>', methods=['GET', 'POST'])
def edit_video(video_id):
    """
    Возвращает страницу редактирования видео
    если пользователь авторизован и является автором,
    иначе доступ запрщен
    """
    form = VideoForm()
    video = db_sess.query(Video).get(video_id)
    if not current_user.is_authenticated:
        return forbidden('')
    if not current_user.id != video.author:
        return forbidden('')

    params = {
        'title': 'Редактирование видео',
        'video': video,
        'description': '\n'.join(video.description.split('<br>')),
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    if form.validate_on_submit():
        video_file = request.files['video']
        if not video_file.filename.lower().endswith('.mp4'):
            return render_template('edit_video.html',
                                   message='Данный формат видео не поддерживается',
                                   form=form, **params)
        preview_file = request.files['preview']
        if not preview_file.filename.lower().endswith('.jpg'):
            return render_template('edit_video.html',
                                   message='Данный формат обложки не поддерживается',
                                   form=form, **params)
        video.title = form.title.data
        video.description = '<br>'.join(form.description.data.split('\n'))
        db_sess.commit()
        video_file.save(os.path.join('static/vid', f'{video.id}.mp4'))
        preview_file.save(os.path.join('static/pre', f'{video.id}.jpg'))
        with Image.open(f'static/pre/{video.id}.jpg') as preview_file:
            width, height = preview_file.size
            if width >= height:
                new_width = height * 16 // 9
                delta = (width - new_width) // 2
                preview_file = preview_file.crop((delta, 0, new_width + delta, height))
            else:
                new_height = width * 9 // 16
                delta = (height - new_height) // 2
                preview_file = preview_file.crop((0, delta, width, new_height + delta))
            preview_file.save(f'static/pre/{video.id}.jpg')
        return redirect('/')
    return render_template('edit_video.html', form=form, **params)


@app.route('/delete_video/<int:video_id>')
def delete_video(video_id):
    video = db_sess.query(Video).get(video_id)
    if not video:
        return page_not_found('Video not found.')


    # No need for explicit comment deletion; should cascade
    db_sess.delete(video)
    db_sess.commit()

    # Remove video file and preview
    try:
        os.remove(os.path.join('static/vid', f'{video.id}.mp4'))
        os.remove(os.path.join('static/pre', f'{video.id}.jpg'))
    except FileNotFoundError:
        pass  # Handle if the files do not exist

    return redirect(f'/')


@app.route('/user/<int:user_id>')
def user(user_id):
    """Возвращает страницу выбранного пользователя с его видео"""
    user = db_sess.query(User).get(user_id)
    if user is None:
        return page_not_found('')

    # Получаем все видео пользователя
    videos = db_sess.query(Video).filter(Video.author_id == user_id).all()[::-1]
    is_birthday = False
    if user.birthday:
        today = datetime.utcnow()
        # Add 3 hours if you need to match a different timezone (e.g., Moscow)
        today_with_offset = today + timedelta(hours=3)
        if (user.birthday.month, user.birthday.day) == (today_with_offset.month, today_with_offset.day):
            is_birthday = True
    # Вычисляем количество видео
    video_count = len(videos)
    # Альтернативно, для оптимизации можно использовать:
    # video_count = db_sess.query(Video).filter(Video.author == user_id).count()

    subscription = False
    if current_user.is_authenticated:
        subscriptions = json.loads(current_user.subscriptions)
        if user_id in subscriptions:
            subscription = True
    print(f"Is user {user_id} subscribed by current user? {'Yes' if subscription else 'No'}")
    # Пагинация видео (разбивка на группы по 3 видео)
    paginated_videos = [videos[i * 3:i * 3 + 3] for i in range(len(videos) // 3)] + [videos[(len(videos) // 3) * 3:]]

    # Добавляем все необходимые параметры, включая video_count
    params = {
        'title': f'{user.name} {user.surname} - Youbox',
        'user': user,
        'videos': paginated_videos,
        'subscription': subscription,
        'empty': True if len(videos) == 0 else False,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user,
        'video_count': video_count,  # Передаём количество видео
        'is_birthday': is_birthday  # Pass if today is their birthday
    }
    return render_template('user.html', **params)
@app.route('/delete_user', methods=['GET', 'POST'])
def delete_user():
    """Handles user deletion with confirmation."""
    if not current_user.is_authenticated:
        return forbidden('')
    if request.method == 'POST':
        # Check for confirmation
        if 'confirm_delete' in request.form:
            # Delete user's videos (assuming you want to cascade deletion)


            # Delete user's comments
            for comment in current_user.comments:
                db_sess.delete(comment)

            # Delete the user
            db_sess.delete(current_user)
            db_sess.commit()

            flash('Your account has been deleted.', 'success')
            return redirect(url_for('index'))  # Redirect to homepage
        else:
            flash('Deletion cancelled.', 'info')
            return redirect(url_for('settings'))  # Redirect back to settings

    # GET request - show confirmation page
    return render_template('delete_user.html', title='Delete Account')
@app.route('/settings')
def settings():


    if not current_user.is_authenticated:
        return forbidden('')
    params = {
        'title': f'Настройки',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('settings.html', **params)
def decline_subscription(number):
    """
    Возвращает правильную форму слова "подписчик" в зависимости от числа.
    Например:
        1 -> подписчик
        2 -> подписчика
        5 -> подписчиков
    """
    number = abs(number)  # Убираем знак, если число отрицательное
    last_two = number % 100
    last = number % 10

    if 11 <= last_two <= 14:
        return "подписчиков"
    if last == 1:
        return "подписчик"
    if 2 <= last <= 4:
        return "подписчика"
    return "подписчиков"
def decline_views(number):
    """
    Возвращает правильную форму слова "подписчик" в зависимости от числа.
    Например:
        1 -> подписчик
        2 -> подписчика
        5 -> подписчиков
    """
    number = abs(number)  # Убираем знак, если число отрицательное
    last_two = number % 100
    last = number % 10

    if 11 <= last_two <= 14:
        return "просмотров"
    if last == 1:
        return "просмотр"
    if 2 <= last <= 4:
        return "просмотра"
    return "просмотров"
# Регистрация фильтра в Jinja2
app.jinja_env.filters['decline_subscription'] = decline_subscription
app.jinja_env.filters['decline_views'] = decline_views
from data.users import User
from sqlalchemy import update

@app.route('/admin/users', methods=['GET', 'POST'])
def admin_users():
    """Административная страница для управления флажками, блокировками и статусом админа пользователей"""
    if not current_user.is_authenticated or not current_user.is_admin:
        return forbidden('')  # Только администратор имеет доступ

    users = db_sess.query(User).all()

    if request.method == 'POST':
        # Обработка изменений флажков, блокировок и статуса админа
        for user in users:
            if user.id == 1:
                continue  # Не изменять статус администратора

            # Обработка флажков is_checked
            is_checked = request.form.get(f'checked_{user.id}', 'off') == 'on'
            if user.is_checked != is_checked:
                user.is_checked = is_checked
                db_sess.add(user)

            # Обработка блокировок is_blocked
            is_blocked = request.form.get(f'blocked_{user.id}', 'off') == 'on'
            if user.is_blocked != is_blocked:
                user.is_blocked = is_blocked
                db_sess.add(user)

            is_tester = request.form.get(f'is_tester_{user.id}', 'off') == 'on'
            if user.is_tester != is_tester:
                user.is_tester = is_tester
                db_sess.add(user)

            # Обработка статуса администратора is_admin
            is_admin = request.form.get(f'is_admin_{user.id}', 'off') == 'on'
            if user.is_admin != is_admin:
                user.is_admin = is_admin
                db_sess.add(user)
        db_sess.commit()
        flash('Изменения успешно сохранены.', 'success')
        return redirect(url_for('admin_users'))

    params = {
        'title': 'Администрирование пользователей',
        'users': users,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('admin_users.html', **params)
@app.route('/feed')
def feed():
    """
    Возвращает страницу с лентой видео от людей,
    на которых подписан пользователь.
    Если пользователь не авторизован, доступ запрещен
    """
    if not current_user.is_authenticated:
        return forbidden('')
    subscriptions = json.loads(current_user.subscriptions)
    videos = []
    for i in subscriptions:
        videos += db_sess.query(Video).filter(Video.author == i).all()[::-1][:12]
    new_videos = sorted(videos, key=lambda x: x.datetime)[::-1]
    new_videos = [
        {
            'video': i,
            'author': db_sess.query(User).get(i.author)
        }
        for i in new_videos
    ]
    params = {
        'title': f'Лента',
        'new_videos': [new_videos[i * 3:i * 3 + 3] for i in range(len(new_videos) // 3)] +
                      [new_videos[(len(new_videos) // 3) * 3:]],
        'empty': True if len(new_videos) == 0 else False,
        'subscripted': True if len(subscriptions) != 0 else False,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('feed.html', **params)


@app.route('/favorite')
def favorite():
    """
    Возвращает страницу с любимыми видео пользователя.
    Если пользователь не авторизован, доступ запрещен
    """
    if not current_user.is_authenticated:
        return forbidden('')
    likes = json.loads(current_user.likes)[::-1]
    liked_videos = [
    {
        'video': video,
        'author': db_sess.query(User).get(video.author) if video.author else None
    }
    for i in likes
    if (video := db_sess.query(Video).get(i)) is not None
]
    params = {
        'title': f'Любимое',
        'liked_videos': [liked_videos[i * 3:i * 3 + 3] for i in range(len(liked_videos) // 3)] +
                        [liked_videos[(len(liked_videos) // 3) * 3:]],
        'empty': True if len(liked_videos) == 0 else False,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('favorite.html', **params)


@app.route('/people')
def people():
    """
    Возвращает страницу с пользователями, на которых подписан пользователь.
    Если пользователь не авторизован, доступ запрещен
    """
    if not current_user.is_authenticated:
        return forbidden('')
    subscriptions = json.loads(current_user.subscriptions)
    subscripted_users = [
        {
            'user': db_sess.query(User).get(i)
        }
        for i in subscriptions
    ]
    subscripted_users.sort(key=lambda x: x['user'].name + x['user'].surname + x['user'].login)
    params = {
        'title': f'Люди',
        'subscripted_users': subscripted_users,
        'empty': True if len(subscripted_users) == 0 else False,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }
    return render_template('people.html', **params)
from data.edit_profile_form import EditProfileForm  # Импортируем форму

# In your main2.py (or appropriate routes file)

@app.route('/edit_profile', methods=['GET', 'POST'])
def edit_profile():
    if not current_user.is_authenticated:
        return forbidden('')

    form = EditProfileForm(obj=current_user)
    params = {
        'title': 'Редактирование профиля',
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }

    if form.validate_on_submit():
        if form.login.data and form.login.data != current_user.login:
            existing_user = db_sess.query(User).filter(User.login == form.login.data).first()
            if existing_user:
                flash('Пользователь с таким логином уже существует.', 'danger')
                return render_template('edit_profile.html', form=form, **params)

        current_user.name = form.name.data
        current_user.surname = form.surname.data
        if form.password.data:
            current_user.password = pwd_context.hash(form.password.data)

        current_user.channel_description = form.channel_description.data

        # Update the birthday field
        current_user.birthday = form.birthday.data if form.birthday.data else None

        # Update theme selection
        current_user.theme_name = form.theme_name.data

        if form.theme_name.data == 'custom':
            current_user.theme_primary_color = str(form.theme_primary_color.data)
            current_user.theme_secondary_color = str(form.theme_secondary_color.data)
            current_user.theme_background_color = str(form.theme_background_color.data)
        else:
            # If not custom, set theme colors to defaults based on theme name
            if form.theme_name.data == 'dark':
                current_user.theme_primary_color = '#343a40'        # Dark gray
                current_user.theme_secondary_color = '#ffffff'      # White
                current_user.theme_background_color = '#212529'     # Darker gray
            elif form.theme_name.data == 'light':
                current_user.theme_primary_color = '#ffffff'        # White
                current_user.theme_secondary_color = '#000000'      # Black
                current_user.theme_background_color = '#f8f9fa'     # Light gray
            else:  # default
                current_user.theme_primary_color = '#007bff'        # Bootstrap primary color
                current_user.theme_secondary_color = '#6c757d'      # Bootstrap secondary color
                current_user.theme_background_color = '#ffffff'     # White

        db_sess.commit()
        flash('Профиль обновлён успешно!', 'success')
        return redirect(url_for('user', user_id=current_user.id))

    return render_template('edit_profile.html', form=form, **params)
@app.route('/api/comment_like/<int:comment_id>', methods=['POST'])
def comment_like(comment_id):
    if not current_user.is_authenticated:
        return {'error': 'Unauthorized'}, 401
    comment = db_sess.query(Comment).get(comment_id)
    if not comment:
        return {'error': 'Comment not found'}, 404
    likes = json.loads(current_user.comment_likes)
    if comment_id in likes:
        return {'error': 'Already liked'}, 400
    likes.append(comment_id)
    current_user.comment_likes = json.dumps(likes)
    comment.likes += 1
    db_sess.commit()
    return {'success': True, 'likes': comment.likes}

@app.route('/api/comment_unlike/<int:comment_id>', methods=['POST'])
def comment_unlike(comment_id):
    if not current_user.is_authenticated:
        return {'error': 'Unauthorized'}, 401
    comment = db_sess.query(Comment).get(comment_id)
    if not comment:
        return {'error': 'Comment not found'}, 404
    likes = json.loads(current_user.comment_likes)
    if comment_id not in likes:
        return {'error': 'Not liked'}, 400
    likes.remove(comment_id)
    current_user.comment_likes = json.dumps(likes)
    comment.likes -= 1
    db_sess.commit()
    return {'success': True, 'likes': comment.likes}
@app.route('/about')
def about():
    params = {
        'title': f'Про сайт'

    }
    return render_template('about.html', **params)
from flask import request, render_template
from sqlalchemy import or_


@app.route('/найти')
@app.route('/search')
def search():
    query = request.args.get('search', '').strip().lower()
    results = []

    if query:
        search_pattern = f"%{query}%"

        # Perform case-insensitive search on title, description, and author's full name
        results = db_sess.query(Video).join(User).filter(
            db.or_(
                func.lower(Video.title).like(search_pattern),
                func.lower(Video.description).like(search_pattern),
                func.lower(func.concat(User.name, ' ', User.surname)).like(search_pattern)
            )
        ).all()

    return render_template('search_results.html', videos=results, query=query)
@app.route('/users')
def users():
    # Query all users from the database
    all_users = db_sess.query(User).all()

    # Set up template parameters
    params = {
        'title': 'All Users',
        'users': all_users,
        'authenticated': current_user.is_authenticated,
        'current_user': current_user
    }

    # Render the template with the list of users
    return render_template('users.html', **params)



db_session.global_init("db/hosting.sql")

db_sess = db_session.create_session()
SqlAlchemyBase.metadata.create_all(bind=db_sess.bind)
db_sess.rollback

SqlAlchemyBase.metadata.create_all(bind=db_sess.bind)
api.add_resource(UsersResource, '/api/users/<int:user_id>')
api.add_resource(UserListResource, '/api/users')
api.add_resource(VideosResource, '/api/videos/<int:video_id>')
api.add_resource(VideoListResource, '/api/videos')
api.add_resource(LikeResource, '/api/like/<int:video_id>')
api.add_resource(NotLikeResource, '/api/not_like/<int:video_id>')
api.add_resource(FollowResource, '/api/follow/<int:user_id>')
api.add_resource(NotFollowResource, '/api/not_follow/<int:user_id>')


if __name__ == '__main__':
    from waitress import serve
    app.run(port=8080, host='0.0.0.0')
@socketio.on('connect')
def handle_connect(*args, **kwargs):
    if current_user.is_authenticated:
        with db_sess.begin():  # Автоматически начнёт новую транзакцию
            user = db_sess.query(User).get(current_user.id)
            if user:
                sid = request.sid

                # Обновляем список SID
                user_sids = json.loads(user.sid_list)
                user_sids.append(sid)
                user.sid_list = json.dumps(user_sids)

                user.is_online = True
                db_sess.commit()
                emit('user_status', {'user_id': user.id, 'status': 'online'}, broadcast=True, include_self=False)
                print('включил')
    else:
        disconnect()

# Обработчик отключения
@socketio.on('disconnect')
def handle_disconnect():
    user = db_sess.query(User).get(current_user.id)
    sid = request.sid
    user_sid = db_sess.query(UserSID).filter_by(sid=sid).first()
    if user_sid:
        user = user_sid.user
        # Удаляем SID из списка
        user_sids = json.loads(user.sid_list)
        if sid in user_sids:
            user_sids.remove(sid)
            user.sid_list = json.dumps(user_sids)

        # Если больше нет активных SID, ставим is_online в False
        if not user_sids:
            user.is_online = False
            print('выключил')

        db_sess.commit()
        emit('user_status', {'user_id': user.id, 'status': 'offline'}, broadcast=True, include_self=False)
    else:
        print(f"SID {sid} not found in any user.")
help fixing this part, the disconnect and connect do not work properly.
Rewrrite the code, so it has ONLY:
make online=true on connect and 
online=false on disconnect