diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index efa9294..2e71ed4 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,9 @@ NeTube/ - **Улучшение дизайна:** Сделайте интерфейс более привлекательным и удобным. - **Расширенная система поиска:** Внедрите более мощный поиск видео по названию, описанию, тегам. -**Лицензия:** [[MIT]](https://github.com/SL1dee36/NeTube/blob/main/LICENSE) +**Лицензия:** + +[MIT] ![image](https://github.com/user-attachments/assets/b7629010-373d-4c55-8780-b8cdce19d24d) diff --git a/app.py b/app.py new file mode 100644 index 0000000..0428ee0 --- /dev/null +++ b/app.py @@ -0,0 +1,57 @@ +# app.py +import traceback + +from flask import Flask, render_template +from markupsafe import Markup + +from helpers.utils import get_current_user +from models import db +import logging +from config import Config +from routes import register_routes + +app = Flask(__name__) +app.config.from_object(Config) +db.init_app(app) + +# Конфигурация логгера +logging.basicConfig(level=logging.DEBUG) + +# Register routes +register_routes(app) + + +# Регистрация обработчиков ошибок +@app.errorhandler(400) +def bad_request_error(error): + current_user = get_current_user() + return render_template('errors/400.html',current_user=current_user, Markup=Markup), 400 + +@app.errorhandler(401) +def unauthorized_error(error): + current_user = get_current_user() + return render_template('errors/401.html',current_user=current_user, Markup=Markup), 401 + +@app.errorhandler(403) +def forbidden_error(error): + current_user = get_current_user() + return render_template('errors/403.html',current_user=current_user, Markup=Markup), 403 + +@app.errorhandler(404) +def not_found_error(error): + current_user = get_current_user() + return render_template('errors/404.html',current_user=current_user, Markup=Markup), 404 + + +@app.errorhandler(500) +def internal_server_error(error): + current_user = get_current_user() + + return render_template('errors/500.html', current_user=current_user, Markup=Markup, error=error), 500 + + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(debug=True) diff --git a/config.py b/config.py new file mode 100644 index 0000000..4a609f5 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +import os + +class Config: + SQLALCHEMY_DATABASE_URI = 'sqlite:///netube.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + UPLOAD_FOLDER = os.path.join(os.getcwd(), 'static/videos') + THUMBNAIL_FOLDER = os.path.join(os.getcwd(), 'static/thumbnails') + ICONS_FOLDER = os.path.join(os.getcwd(), 'static/avatars') + AVATARS_FOLDER = os.path.join(os.getcwd(), 'static/avatars/users') + VIDEOS_FOLDER = os.path.join(os.getcwd(), 'static/videos') + MAX_CONTENT_LENGTH = 8 * 1024 * 1024 * 1024 # 8GB + SECRET_KEY = 'your_secret_key' # Change to a secure key + ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'mp4'} + ADMIN_SECRET_KEY = "123" # Change this in production + diff --git a/helpers/utils.py b/helpers/utils.py new file mode 100644 index 0000000..7dd71a7 --- /dev/null +++ b/helpers/utils.py @@ -0,0 +1,82 @@ +# utils.py + +import random +import string +from flask import session +from fuzzywuzzy import process, fuzz +from moviepy.editor import VideoFileClip +from PIL import Image + +from config import Config +from models import User, Video # Импортируйте модель User + + + +def allowed_file(filename): + """Проверяет, является ли файл допустимого формата.""" + return '.' in filename and filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS + + +def generate_thumbnail(video_path, thumbnail_path): + """ + Генерирует миниатюру видео, сохраняя ее в нужном размере и соотношении сторон. + + Args: + video_path (str): Путь к видеофайлу. + thumbnail_path (str): Путь для сохранения миниатюры. + """ + video = VideoFileClip(video_path) + frame = video.get_frame(t=0.0) + image = Image.fromarray(frame) + width, height = image.size + + # Обрезка для сохранения соотношения 16:9 + if width / height != 16 / 9: + crop_width = width if width > height else int(height * 16 / 9) + crop_height = int(crop_width * 9 / 16) if width > height else height + left_offset = (width - crop_width) // 2 + top_offset = (height - crop_height) // 2 + image = image.crop((left_offset, top_offset, left_offset + crop_width, top_offset + crop_height)) + + # Сжатие до 1280x720, если необходимо + if width > 1280 or height > 720: + image = image.resize((1280, 720)) + + image.save(thumbnail_path) + + +def generate_random_name(length=10): + """Генерирует случайное имя заданной длины из букв и цифр.""" + letters = string.ascii_letters + string.digits + return ''.join(random.choice(letters) for _ in range(length)) + + +def get_current_user(): + """Возвращает текущего пользователя на основе данных сессии.""" + user_id = session.get('user_id') + if user_id: + return User.query.get(user_id) + return None + + +def search_videos(search_query, videos): + """ + Выполняет нечеткий поиск видео по названию. + + Args: + search_query (str): Поисковый запрос. + videos (list): Список видео объектов. + + Returns: + list: Список найденных видео. + """ + # Получаем заголовки видео для поиска + video_titles = [(video, video.title) for video in videos] + + # Выполняем нечеткий поиск по названиям видео + matches = process.extract(search_query, [title for video, title in video_titles], scorer=fuzz.token_sort_ratio) + + # Оставляем только те результаты, где совпадение больше или равно 20 + search_results = [video for (video, title), score in zip(video_titles, matches) if score[1] >= 20] + + return search_results diff --git a/images/demo (1).png b/images/demo (1).png deleted file mode 100644 index dc815b3..0000000 Binary files a/images/demo (1).png and /dev/null differ diff --git a/images/demo (2).png b/images/demo (2).png deleted file mode 100644 index a4e920e..0000000 Binary files a/images/demo (2).png and /dev/null differ diff --git a/instance/netube.db b/instance/netube.db deleted file mode 100644 index dcf0f47..0000000 Binary files a/instance/netube.db and /dev/null differ diff --git a/src/models.py b/models.py similarity index 97% rename from src/models.py rename to models.py index 298bb6b..ce6bab1 100644 --- a/src/models.py +++ b/models.py @@ -1,55 +1,54 @@ -from flask_sqlalchemy import SQLAlchemy -import datetime -from datetime import datetime, timezone - -db = SQLAlchemy() - -# Association table for subscriptions -subscriptions = db.Table('subscriptions', - db.Column('subscriber_id', db.Integer, db.ForeignKey('user.id')), - db.Column('subscribed_to_id', db.Integer, db.ForeignKey('user.id')) -) - -class User(db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(80), unique=True, nullable=False) - password = db.Column(db.String(120), nullable=False) - avatar = db.Column(db.String(255), nullable=True) - videos = db.relationship('Video', backref='author', lazy=True) - # Relationships for subscribers and subscriptions - subscribed_to = db.relationship('User', secondary='subscriptions', - primaryjoin=(subscriptions.c.subscriber_id == id), - secondaryjoin=(subscriptions.c.subscribed_to_id == id), - backref=db.backref('subscribers', lazy='dynamic'), - lazy='dynamic') - -class Video(db.Model): - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(120), nullable=False) - url = db.Column(db.String(200), nullable=False) - description = db.Column(db.Text(500), nullable=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - likes = db.Column(db.Integer, default=0) - dislikes = db.Column(db.Integer, default=0) - comments = db.relationship('Comment', lazy=True) # Removed backref - views = db.Column(db.Integer, default=0) - -class Like(db.Model): - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) - -class DisLike(db.Model): - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) - -class Comment(db.Model): - id = db.Column(db.Integer, primary_key=True) - content = db.Column(db.Text, nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - timestamp = db.Column(db.DateTime, default=datetime.now(timezone.utc)) - user = db.relationship('User', backref='comments') - - video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) +from flask_sqlalchemy import SQLAlchemy +import datetime +from datetime import datetime, timezone + +db = SQLAlchemy() + +# Association table for subscriptions +subscriptions = db.Table('subscriptions', + db.Column('subscriber_id', db.Integer, db.ForeignKey('user.id')), + db.Column('subscribed_to_id', db.Integer, db.ForeignKey('user.id')) +) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(120), nullable=False) + avatar = db.Column(db.String(255), nullable=True) + videos = db.relationship('Video', backref='author', lazy=True) + # Relationships for subscribers and subscriptions + subscribed_to = db.relationship('User', secondary='subscriptions', + primaryjoin=(subscriptions.c.subscriber_id == id), + secondaryjoin=(subscriptions.c.subscribed_to_id == id), + backref=db.backref('subscribers', lazy='dynamic'), + lazy='dynamic') + +class Video(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(120), nullable=False) + url = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text(500), nullable=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + likes = db.Column(db.Integer, default=0) + dislikes = db.Column(db.Integer, default=0) + comments = db.relationship('Comment', lazy=True) # Removed backref + views = db.Column(db.Integer, default=0) +class Like(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) + +class DisLike(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) + +class Comment(db.Model): + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.Text, nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + timestamp = db.Column(db.DateTime, default=datetime.now(timezone.utc)) + user = db.relationship('User', backref='comments') + + video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) video = db.relationship('Video') \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..6a19d47 --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1,19 @@ +from .channel_routes import channel_bp +from .main_routes import main_bp +from .static_routes import static_bp +from .video_like_routes import video_like_bp +from .video_routes import video_bp +from .subscription_routes import subscription_bp +from .user_routes import user_bp +from .admin_routes import admin_bp + +def register_routes(app): + app.register_blueprint(video_bp) + app.register_blueprint(user_bp) + app.register_blueprint(admin_bp) + app.register_blueprint(main_bp) + app.register_blueprint(channel_bp) + app.register_blueprint(subscription_bp) + app.register_blueprint(static_bp) + app.register_blueprint(video_like_bp) + diff --git a/routes/admin_routes.py b/routes/admin_routes.py new file mode 100644 index 0000000..1bd0554 --- /dev/null +++ b/routes/admin_routes.py @@ -0,0 +1,90 @@ +# routes/admin_routes.py + +from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app as app, abort + +from config import Config +from models import User, Video, db +from helpers.utils import get_current_user, allowed_file +from werkzeug.utils import secure_filename +import os + +admin_bp = Blueprint('admin_bp', __name__) + +@admin_bp.route('/edit_user/', methods=['GET', 'POST']) +def edit_user(user_id): + user = User.query.get_or_404(user_id) + current_user = get_current_user() + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + avatar = request.files.get('avatar') + + user.username = username + if password: + user.password = password + if avatar and allowed_file(avatar.filename): + filename = secure_filename(avatar.filename) + avatar.save(os.path.join(app.config['AVATARS_FOLDER'], filename)) + user.avatar = filename + + # Добавление/удаление подписчиков + add_subscriber = request.form.get('add_subscriber') + remove_subscriber = request.form.get('remove_subscriber') + if add_subscriber: + subscriber = User.query.filter_by(username=add_subscriber).first() + if subscriber and subscriber not in user.subscribers: + user.subscribers.append(subscriber) + if remove_subscriber: + subscriber = User.query.filter_by(username=remove_subscriber).first() + if subscriber and subscriber in user.subscribers: + user.subscribers.remove(subscriber) + + # Удаление видео + delete_video_id = request.form.get('delete_video_id') + if delete_video_id: + video_to_delete = Video.query.get(int(delete_video_id)) + if video_to_delete and video_to_delete.author == user: + db.session.delete(video_to_delete) + # Удаляем файл видео и миниатюру + video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_to_delete.url) + thumbnail_path = os.path.join(app.config['THUMBNAIL_FOLDER'], video_to_delete.url[:-4] + ".jpg") + if os.path.exists(video_path): + os.remove(video_path) + if os.path.exists(thumbnail_path): + os.remove(thumbnail_path) + + db.session.commit() + flash('Пользователь успешно обновлен!', 'success') + return redirect(url_for('admin_bp.administration')) + + return render_template('admin/edit_user.html', user=user, current_user=current_user) + +@admin_bp.route('/edit_video/', methods=['GET', 'POST']) +def edit_video(video_id): + video = Video.query.get_or_404(video_id) + current_user = get_current_user() + + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + + video.title = title + video.description = description + db.session.commit() + flash('Видео успешно обновлено!', 'success') + return redirect(url_for('admin_bp.administration')) + + return render_template('admin/edit_video.html', video=video, current_user=current_user) + +@admin_bp.route('/administration', methods=['GET', 'POST']) +def administration(): + if request.method == 'POST': + entered_key = request.form.get('admin_key') + if entered_key == Config.ADMIN_SECRET_KEY: + users = User.query.all() + videos = Video.query.all() + return render_template('admin/administration.html', users=users, videos=videos) + else: + return "Неверный пароль!" + return render_template('admin/admin_login.html') \ No newline at end of file diff --git a/routes/channel_routes.py b/routes/channel_routes.py new file mode 100644 index 0000000..75ddc64 --- /dev/null +++ b/routes/channel_routes.py @@ -0,0 +1,33 @@ +# routes/channel_routes.py + +from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app as app +from markupsafe import Markup + +from models import User, Video, db +from helpers.utils import get_current_user, allowed_file +import os + +channel_bp = Blueprint('channel_bp', __name__) + +@channel_bp.route('/channel/') +def channel(username): + user = User.query.filter_by(username=username).first_or_404() + videos = Video.query.filter_by(user_id=user.id).all() + current_user = get_current_user() + avatar = current_user.avatar if current_user else None + return render_template('channel/channel.html', user=user, videos=videos, avatar=avatar, current_user=current_user, + Markup=Markup) + +@channel_bp.route('/channel//avatar', methods=['GET', 'POST']) +def upload_avatar(username): + user = User.query.filter_by(username=username).first_or_404() + current_user = get_current_user() + if request.method == 'POST': + avatar = request.files.get('avatar') + if avatar and allowed_file(avatar.filename): + filename = f"{user.username}_avatar.{avatar.filename.rsplit('.', 1)[1]}" + avatar.save(os.path.join(app.config['AVATARS_FOLDER'], filename)) + user.avatar = filename + db.session.commit() + flash('Аватар успешно обновлен!', 'success') + return redirect(url_for('channel_bp.channel', username=username, user=user, current_user=current_user)) diff --git a/routes/main_routes.py b/routes/main_routes.py new file mode 100644 index 0000000..2318b66 --- /dev/null +++ b/routes/main_routes.py @@ -0,0 +1,32 @@ +# routes/main_routes.py + +from flask import Blueprint, render_template, request +from markupsafe import Markup + +from helpers.utils import search_videos +from models import Video +from helpers.utils import get_current_user + +main_bp = Blueprint('main_bp', __name__) + + +@main_bp.route('/', methods=['GET', 'POST']) +def index(): + current_user = get_current_user() + videos = Video.query.order_by(Video.likes.desc()).all() + search_results = [] + search_query = '' + if request.method == 'POST': + search_query = request.form.get('search_query') + if search_query: + search_results = search_videos(search_query, videos) + + + return render_template( + 'index.html', + videos=videos, + search_results=search_results, + Markup=Markup, + current_user=current_user, + search_query=search_query + ) diff --git a/routes/static_routes.py b/routes/static_routes.py new file mode 100644 index 0000000..8e517e1 --- /dev/null +++ b/routes/static_routes.py @@ -0,0 +1,21 @@ +# routes/static_routes.py + +from flask import Blueprint, send_from_directory, current_app as app + +static_bp = Blueprint('static_bp', __name__) + +@static_bp.route('/static/videos/') +def serve_videos(filename): + return send_from_directory(app.config['VIDEOS_FOLDER'], filename) + +@static_bp.route('/static/thumbnails/') +def serve_thumbnails(filename): + return send_from_directory(app.config['THUMBNAIL_FOLDER'], filename) + +@static_bp.route('/static/avatars/') +def serve_icons(filename): + return send_from_directory(app.config['ICONS_FOLDER'], filename) + +@static_bp.route('/static/avatars/users/') +def serve_avatars(filename): + return send_from_directory(app.config['AVATARS_FOLDER'], filename) diff --git a/routes/subscription_routes.py b/routes/subscription_routes.py new file mode 100644 index 0000000..22c96b3 --- /dev/null +++ b/routes/subscription_routes.py @@ -0,0 +1,27 @@ +# routes/subscription_routes.py + +from flask import Blueprint, request, jsonify, session +from models import User +from helpers.utils import get_current_user +from services.video_service import update_subscription + +subscription_bp = Blueprint('subscription_bp', __name__) + +@subscription_bp.route('/api/subscribe/', methods=['POST']) +def subscribe(user_id): + current_user = get_current_user() + if current_user: + user_to_subscribe = User.query.get(user_id) + response = update_subscription(current_user, user_to_subscribe, subscribe=True) + return jsonify(response) + return jsonify({'status': 'error', 'message': 'User not logged in'}), 401 + + +@subscription_bp.route('/api/unsubscribe/', methods=['POST']) +def unsubscribe(user_id): + current_user = get_current_user() + if current_user: + user_to_unsubscribe = User.query.get(user_id) + response = update_subscription(current_user, user_to_unsubscribe, subscribe=False) + return jsonify(response) + return jsonify({'status': 'error', 'message': 'User not logged in'}), 401 diff --git a/routes/user_routes.py b/routes/user_routes.py new file mode 100644 index 0000000..b959cac --- /dev/null +++ b/routes/user_routes.py @@ -0,0 +1,46 @@ +# routes/user_routes.py + +from flask import Blueprint, render_template, request, redirect, url_for, session +from markupsafe import Markup + +from models import User, db +from helpers.utils import get_current_user + +user_bp = Blueprint('user_bp', __name__) + +@user_bp.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + user = User.query.filter_by(username=username).first() + if user and user.password == password: + session['logged_in'] = True + session['user_id'] = user.id + return redirect(url_for('main_bp.index')) + else: + return render_template('user/login.html', error="Неверный логин или пароль") + current_user = get_current_user() + return render_template('user/login.html', current_user=current_user, + Markup=Markup) + +@user_bp.route('/logout') +def logout(): + session.pop('logged_in', None) + session.pop('user_id', None) + return redirect(url_for('main_bp.index')) + +@user_bp.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + if User.query.filter_by(username=username).first(): + return render_template('user/register.html', error="Пользователь с таким именем уже существует") + new_user = User(username=username, password=password) + db.session.add(new_user) + db.session.commit() + return redirect(url_for('user_bp.login')) + current_user = get_current_user() + return render_template('user/register.html', current_user=current_user, + Markup=Markup, ) diff --git a/routes/video_like_routes.py b/routes/video_like_routes.py new file mode 100644 index 0000000..09f9d89 --- /dev/null +++ b/routes/video_like_routes.py @@ -0,0 +1,65 @@ +from flask import Blueprint, jsonify, request +from models import Video, Like, DisLike, db +from helpers.utils import get_current_user + +video_like_bp = Blueprint('video_like_bp', __name__) + + +@video_like_bp.route('/api/like/', methods=['POST']) +def like_video(video_name): + video = Video.query.filter_by(url=video_name).first_or_404() + user = get_current_user() + if user: + existing_like = Like.query.filter_by(user_id=user.id, video_id=video.id).first() + existing_dislike = DisLike.query.filter_by(user_id=user.id, video_id=video.id).first() + + if existing_like: + # Убираем лайк + db.session.delete(existing_like) + video.likes -= 1 + liked = False + else: + # Ставим лайк + if existing_dislike: + # Убираем дизлайк + db.session.delete(existing_dislike) + video.dislikes -= 1 + new_like = Like(user_id=user.id, video_id=video.id) + db.session.add(new_like) + video.likes += 1 + liked = True + + db.session.commit() + + return jsonify({'status': 'success', 'liked': liked, 'likes': video.likes, 'dislikes': video.dislikes}) + return jsonify({'status': 'error', 'message': 'User not authenticated'}), 401 + + +@video_like_bp.route('/api/dislike/', methods=['POST']) +def dislike_video(video_name): + video = Video.query.filter_by(url=video_name).first_or_404() + user = get_current_user() + if user: + existing_like = Like.query.filter_by(user_id=user.id, video_id=video.id).first() + existing_dislike = DisLike.query.filter_by(user_id=user.id, video_id=video.id).first() + + if existing_dislike: + # Убираем дизлайк + db.session.delete(existing_dislike) + video.dislikes -= 1 + disliked = False + else: + # Ставим дизлайк + if existing_like: + # Убираем лайк + db.session.delete(existing_like) + video.likes -= 1 + new_dislike = DisLike(user_id=user.id, video_id=video.id) + db.session.add(new_dislike) + video.dislikes += 1 + disliked = True + + db.session.commit() + + return jsonify({'status': 'success', 'disliked': disliked, 'likes': video.likes, 'dislikes': video.dislikes}) + return jsonify({'status': 'error', 'message': 'User not authenticated'}), 401 diff --git a/routes/video_routes.py b/routes/video_routes.py new file mode 100644 index 0000000..7bfeec1 --- /dev/null +++ b/routes/video_routes.py @@ -0,0 +1,50 @@ +# routes/video_routes.py + +from flask import Blueprint, request, render_template, redirect, url_for, jsonify, session +from markupsafe import Markup +from models import Video +from helpers.utils import get_current_user, allowed_file +from services.video_service import save_video, increment_video_views, add_comment_to_video +video_bp = Blueprint('video_bp', __name__) + +@video_bp.route('/upload', methods=['GET', 'POST']) +def upload(): + current_user = get_current_user() + if 'logged_in' in session: + if request.method == 'POST': + title = request.form['title'] + description = request.form['description'][:500] + file = request.files['file'] + if file and allowed_file(file.filename): + video = save_video(file, title, description, session['user_id']) + return redirect(url_for('video_bp.video', video_name=video)) + return render_template('video/upload.html', current_user=current_user, Markup=Markup, ) + else: + return redirect(url_for('user_bp.login')) + + +@video_bp.route('/video/') +def video(video_name): + video = Video.query.filter_by(url=video_name).first_or_404() + current_user = get_current_user() + increment_video_views(video) + + related_videos = Video.query.filter(Video.id != video.id).limit(9).all() + return render_template('video/video.html', video=video, Markup=Markup, current_user=current_user, related_videos=related_videos) + +@video_bp.route('/api/video//comment', methods=['POST']) +def add_comment(video_name): + video = Video.query.filter_by(url=video_name).first_or_404() + current_user = get_current_user() + + if current_user: + content = request.json.get('content') # Изменено для работы с JSON + if content: + new_comment = add_comment_to_video(video, current_user, content) + return jsonify({ + "message": "Comment added successfully.", + "comment": new_comment + }), 200 + else: + return jsonify({"message": "Content is required."}), 400 + return jsonify({"message": "Unauthorized access."}), 401 diff --git a/services/video_service.py b/services/video_service.py new file mode 100644 index 0000000..ba93fe2 --- /dev/null +++ b/services/video_service.py @@ -0,0 +1,111 @@ +from moviepy.video.io.VideoFileClip import VideoFileClip + +from config import Config +from models import Video, Comment, User, db +from helpers.utils import allowed_file, generate_random_name, generate_thumbnail +from flask import session, jsonify, redirect, url_for +import os +import subprocess + + +def save_video(file, title, description, user_id): + """ + Сохраняет загруженное видео и создает соответствующую запись в базе данных. + + Args: + file: Загружаемый файл. + title (str): Название видео. + description (str): Описание видео. + user_id (int): ID пользователя, который загружает видео. + + Returns: + str: Имя файла. + """ + filename = generate_random_name() + ".mp4" + video_path = os.path.join(Config.UPLOAD_FOLDER, filename) + temp_path = os.path.join(Config.UPLOAD_FOLDER, "temp_" + filename) + + file.save(video_path) + + + # Заменяем исходное видео на измененное + os.rename(temp_path, video_path) + + thumbnail_path = os.path.join(Config.THUMBNAIL_FOLDER, filename[:-4] + ".jpg") + generate_thumbnail(video_path, thumbnail_path) + + new_video = Video( + title=title, + url=filename, + description=description, + user_id=user_id + ) + db.session.add(new_video) + db.session.commit() + + return filename + + +def increment_video_views(video): + """ + Увеличивает количество просмотров видео. + + Args: + video (Video): Объект видео. + """ + video.views += 1 + db.session.commit() + + +def add_comment_to_video(video, current_user, content): + """ + Добавляет комментарий к видео и возвращает созданный комментарий. + + Args: + video (Video): Объект видео. + current_user (User): Текущий пользователь. + content (str): Содержимое комментария. + + Returns: + dict: Данные созданного комментария, включая ID, содержимое и информацию о пользователе. + """ + comment = Comment(content=content, user=current_user, video=video) + db.session.add(comment) + db.session.commit() + + # Возвращаем словарь с данными комментария + return { + "id": comment.id, + "content": comment.content, + "user": { + "username": current_user.username, + "avatar": current_user.avatar or None + } + } + + +def update_subscription(user, user_to_subscribe, subscribe=True): + """ + Обновляет подписку пользователя. + + Args: + user (User): Текущий пользователь. + user_to_subscribe (User): Пользователь, на которого подписываются или отписываются. + subscribe (bool): True для подписки, False для отписки. + + Returns: + dict: Результат обновления подписки. + """ + if subscribe: + user.subscribed_to.append(user_to_subscribe) + else: + user.subscribed_to.remove(user_to_subscribe) + + db.session.commit() + new_count = user_to_subscribe.subscribers.count() + + return { + 'status': 'success', + 'subscribed': subscribe, + 'subscribers': new_count + } \ No newline at end of file diff --git a/src/app.py b/src/app.py deleted file mode 100644 index 5a7ce9d..0000000 --- a/src/app.py +++ /dev/null @@ -1,429 +0,0 @@ -from flask import Flask, request, render_template, redirect, url_for, send_from_directory, session, flash, jsonify - -from models import db, User, Video, Like, Comment, DisLike -import os -from moviepy.editor import VideoFileClip -from PIL import Image -import random -import string -from markupsafe import Markup -from werkzeug.utils import secure_filename -import base64 -from flask import current_app as app -from fuzzywuzzy import fuzz, process -from os import system as s - -s('cls') - -app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///netube.db' -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'src/static/videos') -app.config['THUMBNAIL_FOLDER'] = os.path.join(app.root_path, 'static/thumbnails') -app.config['ICONS_FOLDER'] = os.path.join(app.root_path, 'static/avatars') -app.config['AVATARS_FOLDER'] = os.path.join(app.root_path, 'static/avatars/users') -app.config['VIDEOS_FOLDER'] = os.path.join(app.root_path, 'static/videos') -app.config['MAX_CONTENT_LENGTH'] = 8 * 1024 * 1024 * 1024 # 8GB -app.secret_key = 'your_secret_key' # Замените на ваш случайный секретный ключ -admin_secret_key = "123" # Секретный ключ для админ-панели -db.init_app(app) - -app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'mp4'} - - -def allowed_file(filename): - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] - -def generate_thumbnail(video_path, thumbnail_path): - """ - Генерирует миниатюру видео, сохраняя ее в нужном размере и соотношении сторон. - - Args: - video_path (str): Путь к видеофайлу. - thumbnail_path (str): Путь для сохранения миниатюры. - """ - video = VideoFileClip(video_path) - - frame = video.get_frame(t=0.0) - image = Image.fromarray(frame) - - width, height = image.size - - # Обрезка для сохранения соотношения 16:9 - if width / height != 16 / 9: - # Вычисляем целевую ширину, чтобы получить соотношение 16:9 - if width > height: - crop_width = width - crop_height = int(crop_width * 9 / 16) - else: - crop_height = height - crop_width = int(crop_height * 16 / 9) - - # Вычисляем разницу в ширине и высоте - width_diff = width - crop_width - height_diff = height - crop_height - - # Вычисляем смещения для обрезки - left_offset = width_diff // 2 - top_offset = height_diff // 2 - - # Обрезка изображения - left = left_offset - right = left + crop_width - top = top_offset - bottom = top + crop_height - - image = image.crop((left, top, right, bottom)) - - # Сжатие, если необходимо - if width > 1280 or height > 720: - new_width = 1280 - new_height = int(new_width * 9 / 16) - - image = image.resize((new_width, new_height), Image.LANCZOS) # Используем LANCZOS для лучшего качества сжатия - - # Сохранение миниатюры - image.save(thumbnail_path) - - -def generate_random_name(length=10): - letters = string.ascii_letters + string.digits - return ''.join(random.choice(letters) for i in range(length)) - - -def get_current_user(): - user_id = session.get('user_id') - if user_id is not None: - return User.query.get(user_id) - return None - - -@app.route('/', methods=['GET', 'POST']) -def index(): - current_user = get_current_user() - videos = Video.query.order_by(Video.likes.desc()).all() - search_results = [] - - if request.method == 'POST': - search_query = request.form.get('search_query') - if search_query: - app.logger.debug(f"Search query: {search_query}") - matches = process.extract(search_query, [video.title for video in videos], scorer=fuzz.token_sort_ratio) - app.logger.debug(f"FuzzyWuzzy matches: {matches}") - search_results = [video for video, score in matches if score >= 20] - search_results = [Video.query.filter_by( - title=video.title if isinstance(video.title, str) else str(video.title)).first() for video in - search_results] - search_results = [video for video in search_results if video is not None] - - return render_template('index.html', - videos=videos, - search_results=search_results, - Markup=Markup, - current_user=current_user) - -@app.route('/base', methods=['GET', 'POST']) -def base(): - current_user = get_current_user() - - return render_template('base.html', - Markup=Markup, - current_user=current_user) - -@app.route('/upload', methods=['GET', 'POST']) -def upload(): - current_user = get_current_user() - if 'logged_in' in session: - if request.method == 'POST': - title = request.form['title'] - description = request.form['description'][:500] # Обрезаем описание до 500 символов - file = request.files['file'] - if file and allowed_file(file.filename): - filename = generate_random_name() + ".mp4" - video_path = os.path.join('src/static/videos', filename) - app.logger.debug(f"Saving file to: {video_path}") - file.save(video_path) - thumbnail_path = os.path.join('src/static/thumbnails', filename[:-4] + ".jpg") - generate_thumbnail(video_path, thumbnail_path) - user_id = session['user_id'] - new_video = Video(title=title, url=filename, description=description, user_id=user_id) - db.session.add(new_video) - db.session.commit() - return redirect(url_for('channel', username=User.query.get(user_id).username)) - return render_template('upload.html', current_user=current_user) - else: - return redirect(url_for('login')) - -@app.route('/video/') -def video(video_name): - video = Video.query.filter_by(url=video_name).first_or_404() - current_user = get_current_user() - # Увеличиваем количество просмотров - video.views += 1 - db.session.commit() - - related_videos = Video.query.filter(Video.id != video.id).limit(9).all() - - return render_template('video.html', - video=video, - Markup=Markup, - current_user=current_user, - related_videos=related_videos) - -@app.route('/video//comment', methods=['POST']) -def add_comment(video_name): - video = Video.query.filter_by(url=video_name).first_or_404() - current_user = get_current_user() - if current_user: - content = request.form.get('content') - comment = Comment(content=content, user=current_user, video=video) - db.session.add(comment) - db.session.commit() - return redirect(url_for('video', video_name=video_name)) - -@app.route('/subscribe/', methods=['POST']) -def subscribe(user_id): - user = get_current_user() - if user: - user_to_subscribe = User.query.get(user_id) - user.subscribed_to.append(user_to_subscribe) - db.session.commit() - new_count = user_to_subscribe.subscribers.count() # Получаем обновленное количество - return jsonify({'status': 'success', 'subscribed': True, 'subscribers': new_count}) - return jsonify({'status': 'error', 'message': 'User not logged in'}), 401 - -@app.route('/unsubscribe/', methods=['POST']) -def unsubscribe(user_id): - user = get_current_user() - if user: - user_to_unsubscribe = User.query.get(user_id) - user.subscribed_to.remove(user_to_unsubscribe) - db.session.commit() - new_count = user_to_unsubscribe.subscribers.count() # Получаем обновленное количество - return jsonify({'status': 'success', 'subscribed': False, 'subscribers': new_count}) - return jsonify({'status': 'error', 'message': 'User not logged in'}), 401 - - - -@app.route('/static/videos/') -def serve_videos(filename): - return send_from_directory(app.config['VIDEOS_FOLDER'], filename) - - -@app.route('/static/thumbnails/') -def serve_thumbnails(filename): - return send_from_directory(app.config['THUMBNAIL_FOLDER'], filename) - - -@app.route('/static/avatars/') -def serve_icons(filename): - return send_from_directory(app.config['ICONS_FOLDER'], filename) - -@app.route('/static/avatars/users/') -def serve_avatars(filename): - return send_from_directory(app.config['AVATARS_FOLDER'], filename) - -@app.route('/like/') -def like_video(video_name): - video = Video.query.filter_by(url=video_name).first_or_404() - user = get_current_user() - if user: - existing_like = Like.query.filter_by(user_id=user.id, video_id=video.id).first() - existing_dislike = DisLike.query.filter_by(user_id=user.id, video_id=video.id).first() - - if existing_like: - # Убираем лайк - db.session.delete(existing_like) - video.likes -= 1 - else: - # Ставим лайк - if existing_dislike: - # Убираем дизлайк - db.session.delete(existing_dislike) - video.dislikes -= 1 - new_like = Like(user_id=user.id, video_id=video.id) - db.session.add(new_like) - video.likes += 1 - - db.session.commit() - - return redirect(url_for('video', video_name=video_name)) - - -@app.route('/dislike/') -def dislike_video(video_name): - video = Video.query.filter_by(url=video_name).first_or_404() - user = get_current_user() - if user: - existing_like = Like.query.filter_by(user_id=user.id, video_id=video.id).first() - existing_dislike = DisLike.query.filter_by(user_id=user.id, video_id=video.id).first() - - if existing_dislike: - # Убираем дизлайк - db.session.delete(existing_dislike) - video.dislikes -= 1 - else: - # Ставим дизлайк - if existing_like: - # Убираем лайк - db.session.delete(existing_like) - video.likes -= 1 - new_dislike = DisLike(user_id=user.id, video_id=video.id) - db.session.add(new_dislike) - video.dislikes += 1 - - db.session.commit() - - return redirect(url_for('video', video_name=video_name)) - - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - user = User.query.filter_by(username=username).first() - if user and user.password == password: - session['logged_in'] = True - session['user_id'] = user.id - return redirect(url_for('index')) - else: - return render_template('login.html', error="Неверный логин или пароль") - current_user = get_current_user() - return render_template('login.html', current_user=current_user) - - -@app.route('/logout') -def logout(): - session.pop('logged_in', None) - session.pop('user_id', None) - return redirect(url_for('index')) - - -@app.route('/register', methods=['GET', 'POST']) -def register(): - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - if User.query.filter_by(username=username).first(): - return render_template('register.html', error="Пользователь с таким именем уже существует") - new_user = User(username=username, password=password) - db.session.add(new_user) - db.session.commit() - return redirect(url_for('login')) - current_user = get_current_user() - return render_template('register.html', current_user=current_user) - - -@app.route('/channel/') -def channel(username): - user = User.query.filter_by(username=username).first_or_404() - videos = Video.query.filter_by(user_id=user.id).all() - current_user = get_current_user() - if current_user: - avatar = current_user.avatar - else: - avatar = None - return render_template('channel.html', user=user, videos=videos, avatar=avatar, Markup=Markup, - current_user=current_user) - -@app.route('/channel//avatar', methods=['GET', 'POST']) # Используем один маршрут -def upload_avatar(username): - user = User.query.filter_by(username=username).first_or_404() - current_user = get_current_user() - if request.method == 'POST': - avatar = request.files.get('avatar') - - if avatar and allowed_file(avatar.filename): - filename = f"{user.username}_avatar.{avatar.filename.rsplit('.', 1)[1]}" - avatar.save(os.path.join(app.config['AVATARS_FOLDER'], filename)) - user.avatar = filename - db.session.commit() - flash('Аватар успешно обновлен!', 'success') - return redirect(url_for('channel', username=username, user=user, current_user=current_user)) - -@app.route('/edit_user/', methods=['GET', 'POST']) -def edit_user(user_id): - user = User.query.get_or_404(user_id) - current_user = get_current_user() - - if request.method == 'POST': - username = request.form.get('username') - password = request.form.get('password') - avatar = request.files.get('avatar') - - user.username = username - if password: - user.password = password - if avatar and allowed_file(avatar.filename): - filename = secure_filename(avatar.filename) - avatar.save(os.path.join(app.config['AVATARS_FOLDER'], filename)) - user.avatar = filename - - # Добавление/удаление подписчиков - add_subscriber = request.form.get('add_subscriber') - remove_subscriber = request.form.get('remove_subscriber') - if add_subscriber: - subscriber = User.query.filter_by(username=add_subscriber).first() - if subscriber and subscriber not in user.subscribers: - user.subscribers.append(subscriber) - if remove_subscriber: - subscriber = User.query.filter_by(username=remove_subscriber).first() - if subscriber and subscriber in user.subscribers: - user.subscribers.remove(subscriber) - - # Удаление видео - delete_video_id = request.form.get('delete_video_id') - if delete_video_id: - video_to_delete = Video.query.get(int(delete_video_id)) - if video_to_delete and video_to_delete.author == user: - db.session.delete(video_to_delete) - - # Удаляем файл видео и миниатюру - video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_to_delete.url) - thumbnail_path = os.path.join(app.config['THUMBNAIL_FOLDER'], video_to_delete.url[:-4] + ".jpg") - if os.path.exists(video_path): - os.remove(video_path) - if os.path.exists(thumbnail_path): - os.remove(thumbnail_path) - - db.session.commit() - flash('Пользователь успешно обновлен!', 'success') - return redirect(url_for('administration')) - - return render_template('edit_user.html', user=user, current_user=current_user) - -@app.route('/edit_video/', methods=['GET', 'POST']) -def edit_video(video_id): - video = Video.query.get_or_404(video_id) - current_user = get_current_user() - - if request.method == 'POST': - title = request.form.get('title') - description = request.form.get('description') - - video.title = title - video.description = description - db.session.commit() - flash('Видео успешно обновлено!', 'success') - return redirect(url_for('administration')) - - return render_template('edit_video.html', video=video, current_user=current_user) - -@app.route('/administration', methods=['GET', 'POST']) -def administration(): - if request.method == 'POST': - entered_key = request.form.get('admin_key') - if entered_key == admin_secret_key: - users = User.query.all() - videos = Video.query.all() - return render_template('Administration.html', users=users, videos=videos) - else: - return "Неверный пароль!" - return render_template('admin_login.html') - -if __name__ == '__main__': - with app.app_context(): - db.create_all() - app.run(debug=True) \ No newline at end of file diff --git a/src/instance/netube.db b/src/instance/netube.db deleted file mode 100644 index fbe249f..0000000 Binary files a/src/instance/netube.db and /dev/null differ diff --git a/src/static/auth_page.css b/src/static/auth_page.css deleted file mode 100644 index 60205a8..0000000 --- a/src/static/auth_page.css +++ /dev/null @@ -1,71 +0,0 @@ -/* src/static/auth_page.css */ -body { - font-family: 'Roboto', sans-serif; /* Use Roboto font */ - background-color: #f9f9f9; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: auto; -} - -h1 { - margin-top: 100px; - margin-bottom: 20px; - font-size: 24px; - color: #333; - font-weight: 700; /* Bold title */ -} - -form { - display: flex; - flex-direction: column; - width: 300px; - padding: 20px; - border: 1px solid #ccc; - border-radius: 5px; - background-color: #fff; -} - -input[type="text"], input[type="password"] { - padding: 10px; - margin-bottom: 10px; - border: 1px solid #ccc; - border-radius: 3px; - font-size: 16px; /* Slightly larger input font size */ - color: #333; -} - -button[type="submit"] { - padding: 10px; - border: none; - border-radius: 3px; - background-color: #4CAF50; - color: white; - cursor: pointer; - font-size: 16px; /* Larger button font size */ - font-weight: 700; /* Bold button text */ - transition: background-color 0.3s ease; /* Smooth transition for hover effect */ -} - -button[type="submit"]:hover { - background-color: #45a049; /* Slightly darker green on hover */ -} - -p { - margin-top: 10px; - font-size: 14px; /* Smaller font size for paragraphs */ - color: #555; /* Darker gray for paragraphs */ -} - -p a { - text-decoration: none; - color: #386125; /* Green for links */ - transition: color 0.3s ease; /* Smooth transition for hover effect */ -} - -p a:hover { - color: #45a049; /* Darker green on hover */ -} \ No newline at end of file diff --git a/src/static/channel_page.css b/src/static/channel_page.css deleted file mode 100644 index 3c24503..0000000 --- a/src/static/channel_page.css +++ /dev/null @@ -1,236 +0,0 @@ -body { - font-family: 'Material Icons', sans-serif; - background-color: #f9f9f9; - margin: 0; - padding: 0; -} - - - -header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - background-color: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -header h1 { - margin: 0; - font-size: 24px; - color: #333; -} - -header nav { - display: flex; - align-items: center; -} - -header nav a { - display: flex; - align-items: center; - text-decoration: none; - color: inherit; - margin-left: 15px; -} - -header nav a img { - border-radius: 50%; - width: 30px; - height: 30px; -} - -.channel-container { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 20px; -} - -.channel-container h2 { - margin-bottom: 10px; - font-size: 22px; - color: #333; -} - -.upload-form { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 20px; -} - -.upload-button, .submit-button { - display: inline-flex; - align-items: center; - padding: 10px 20px; - margin: 10px 0; - font-size: 16px; - font-weight: bold; - color: #fff; - background-color: #4CAF50; /* Зеленый цвет */ - border: none; - border-radius: 4px; /* Более скругленные углы */ - cursor: pointer; - text-align: center; - text-decoration: none; - transition: background-color 0.3s ease, box-shadow 0.3s ease; /* Добавляем transition для box-shadow */ - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); /* Тень */ -} - -.upload-button:hover, .submit-button:hover { - background-color: #45a049; /* Более темный зеленый при наведении */ - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); /* Увеличиваем тень при наведении */ -} - -.upload-button i, .submit-button i { - margin-right: 5px; - font-size: 18px; -} - -.upload-button:hover, .submit-button:hover { - background-color: #0056b3; -} - -.upload-button i, .submit-button i { - margin-right: 5px; - font-size: 18px; -} - -.video-container { - display: flex; - flex-wrap: wrap; - justify-content: center; - padding: 20px; - color: inherit; - text-decoration: none; -} - -.video-preview { - width: 320px; - margin: 15px; - border-radius: 10px; - overflow: hidden; - background-color: #ffffff00; - transition: transform 0.3s, box-shadow 0.3s; - padding: 5px; - color: inherit; - text-decoration: none; -} - -header a:link, header a:visited { - color: inherit; - text-decoration: none; -} - -header a:hover, header a:active { - color: inherit; - text-decoration: none; -} - -.video-preview:hover { - transform: scale(1.0); - background-color: #fff; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); - color: inherit; - text-decoration: none; -} - -.video-preview img { - width: 100%; - height: auto; - object-fit: cover; - border-radius: 10px; - transition: transform 0.3s; -} - -.video-preview:hover img { - transform: scale(1.01); - color: inherit; - text-decoration: none; -} - -.video-preview h3 { - margin: 10px 0 5px; - font-size: 16px; - color: #333; - display: flex; - align-items: center; -} - -.video-preview h3 img { - width: 30px; - height: 30px; - object-fit: cover; - border-radius: 50%; - margin-right: 10px; -} - -.video-preview p { - margin: 0; - font-size: 14px; - color: #666; -} - -header a:link, header a:visited { - color: inherit; - text-decoration: none; -} - -header a:hover, header a:active { - color: inherit; - text-decoration: none; -} - -#avatar-button { - border-radius: 50%; /* Скругление в форме круга */ - border: none; /* Убираем границу */ - padding: 0; /* Убираем внутренние отступы */ - background: none; /* Убираем фоновый цвет */ - cursor: pointer; /* Изменяем курсор на указатель */ -} - -#avatar-button:hover { - /* Добавьте стили при наведении курсора, например, изменение цвета */ - transform: scale(1.01); -} - -.subscribe-button { - background-color: #ff0000; /* Синий цвет */ - color: white; - border: none; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 12px; - border-radius: 5px; /* Скругленные углы */ - cursor: pointer; -} - -.unsubscribe-button { - background-color: #3e3e3e; /* Красный цвет */ - color: white; - border: none; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 12px; - border-radius: 5px; /* Скругленные углы */ - cursor: pointer; -} - -a { - text-decoration: none; - color: inherit; -} -a:hover, a:active { - text-decoration: none; - color: inherit; -} - -img { - border-radius: 50% -} \ No newline at end of file diff --git a/src/static/index_page.css b/src/static/index_page.css deleted file mode 100644 index 2914897..0000000 --- a/src/static/index_page.css +++ /dev/null @@ -1,268 +0,0 @@ -/* src/static/index_page.css */ - -header .search-container { - display: flex; - justify-content: center; /* Горизонтальное центрирование */ - align-items: center; /* Вертикальное центрирование */ - margin: 0 auto; /* Центрируем контейнер */ -} - -header .search-container input[type="text"] { - padding: 8px; - width: 500px; - max-width: 500px; - border: 1px solid #ccc; - border-radius: 5px 0 0 5px; - font-size: 13px; -} - -header .search-container button { - background-color: #ffffff; - border: 1px solid #ccc; - border-radius: 0 5px 5px 0; - padding: 8px 12px; - cursor: pointer; -} - -/* Медиа-запрос для телефонов */ -@media (max-width: 768px) { - header .search-container input[type="text"] { - width: 80%; - max-width: 150px; - } -} - -body { - font-family: 'Material Icons', sans-serif; - background-color: #f9f9f9; - margin: 0; - padding: 0; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - background-color: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -header h1 { - margin: 0; - font-size: 24px; - color: #333; -} - -header nav { - display: flex; - align-items: center; -} - -header nav a { - display: flex; - align-items: center; - text-decoration: none; - color: inherit; - margin-left: 15px; -} - -header nav a img { - border-radius: 50%; - width: 30px; - height: 30px; -} - -.channel-container { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 20px; -} - -.channel-container h2 { - margin-bottom: 10px; - font-size: 22px; - color: #333; -} - -.upload-form { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 20px; -} - -.upload-button, .submit-button { - display: inline-flex; - align-items: center; - padding: 10px 20px; - margin: 10px 0; - font-size: 16px; - font-weight: bold; - color: #fff; - background-color: #4CAF50; /* Зеленый цвет */ - border: none; - border-radius: 4px; /* Более скругленные углы */ - cursor: pointer; - text-align: center; - text-decoration: none; - transition: background-color 0.3s ease, box-shadow 0.3s ease; /* Добавляем transition для box-shadow */ - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); /* Тень */ -} - -.upload-button:hover, .submit-button:hover { - background-color: #45a049; /* Более темный зеленый при наведении */ - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); /* Увеличиваем тень при наведении */ -} - -.upload-button i, .submit-button i { - margin-right: 5px; - font-size: 18px; -} - -.upload-button:hover, .submit-button:hover { - background-color: #0056b3; -} - -.upload-button i, .submit-button i { - margin-right: 5px; - font-size: 18px; -} - -.video-container { - display: flex; - flex-wrap: wrap; - justify-content: center; - padding: 20px; - color: inherit; - text-decoration: none; -} - -.video-preview { - width: 320px; - margin: 15px; - border-radius: 10px; - overflow: hidden; - background-color: #ffffff00; - transition: transform 0.3s, box-shadow 0.3s; - padding: 5px; - color: inherit; - text-decoration: none; -} - -header a:link, header a:visited { - color: inherit; - text-decoration: none; -} - -header a:hover, header a:active { - color: inherit; - text-decoration: none; -} - -.video-preview:hover { - transform: scale(1.0); - background-color: #fff; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); - color: inherit; - text-decoration: none; -} - -.video-preview img { - width: 100%; - height: auto; - object-fit: cover; - border-radius: 10px; - transition: transform 0.3s; -} - -.video-preview:hover img { - transform: scale(1.01); - color: inherit; - text-decoration: none; -} - -.video-preview h3 { - margin: 10px 0 5px; - font-size: 16px; - color: #333; - display: flex; - align-items: center; -} - -.video-preview h3 img { - width: 30px; - height: 30px; - object-fit: cover; - border-radius: 50%; - margin-right: 10px; -} - -.video-preview p { - margin: 0; - font-size: 14px; - color: #666; -} - -header a:link, header a:visited { - color: inherit; - text-decoration: none; -} - -header a:hover, header a:active { - color: inherit; - text-decoration: none; -} - -#avatar-button { - border-radius: 50%; /* Скругление в форме круга */ - border: none; /* Убираем границу */ - padding: 0; /* Убираем внутренние отступы */ - background: none; /* Убираем фоновый цвет */ - cursor: pointer; /* Изменяем курсор на указатель */ -} - -#avatar-button:hover { - /* Добавьте стили при наведении курсора, например, изменение цвета */ - transform: scale(1.01); -} - -.subscribe-button { - background-color: #ff0000; /* Синий цвет */ - color: white; - border: none; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 12px; - border-radius: 5px; /* Скругленные углы */ - cursor: pointer; -} - -.unsubscribe-button { - background-color: #3e3e3e; /* Красный цвет */ - color: white; - border: none; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 12px; - border-radius: 5px; /* Скругленные углы */ - cursor: pointer; -} - -a { - text-decoration: none; - color: inherit; -} -a:hover, a:active { - text-decoration: none; - color: inherit; -} - -img { - border-radius: 50% -} \ No newline at end of file diff --git a/src/static/style.css b/src/static/style.css deleted file mode 100644 index 599be1c..0000000 --- a/src/static/style.css +++ /dev/null @@ -1,115 +0,0 @@ -/* src/static/styles.css */ - -.body { - font-family: 'Material Icons', sans-serif; -} - -#menu-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0); /* Начальное состояние: прозрачный фон */ - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; - opacity: 0; /* Начальное состояние: невидимый */ - pointer-events: none; /* Начальное состояние: не реагирует на клики */ - transition: opacity 0.15s ease-in-out; /* Плавное изменение прозрачности */ -} - -#menu-overlay.open { - opacity: 1; /* Открытое состояние: видимый */ - pointer-events: auto; /* Открытое состояние: реагирует на клики */ - background-color: rgba(0, 0, 0, 0.5); /* Открытое состояние: полупрозрачный фон */ -} - -#menu-content { - background-color: white; - padding: 20px; - border-radius: 10px; /* Скругленные углы */ - width: 400px; - height: 400px; - display: flex; - flex-direction: column; /* Располагаем блоки вертикально */ -} - -#menu-content > * { /* Выбираем все прямые дочерние элементы #menu-content */ - margin-bottom: 15px; /* Отступ между блоками */ - /* border-bottom: 1px solid #ccc; Разделительная линия */ - /* padding-bottom: 15px; Отступ снизу от линии */ -} - -#menu-content > *:last-child { /* Убираем линию у последнего блока */ - border-bottom: none; - margin-bottom: 0; -} - -.menu-block { - /* padding: 10px; */ - box-sizing: border-box; /* Добавляем эту строку */ -} - -.separator { - width: auto; /* Например, 90% от ширины #menu-content */ - border-bottom: 1px solid #ccc; -} - -.vert-separator { - height: 30px; /* Высота сепаратора */ - width: 1px; /* Толщина линии */ - background-color: #ccc; /* Цвет линии */ - margin: 15px auto; /* Отступы сверху/снизу, центрирование по горизонтали */ -} - -.hidden { - display: none; -} - - -.user-info { - display: flex; /* Располагаем аватарку и детали пользователя в строку */ - align-items: center; /* Вертикальное выравнивание */ -} - -.user-details { - margin-left: 10px; - display: flex; - flex-direction: column; /* Располагаем тег и подписчиков вертикально */ -} - -.username { - font-size: 18px; /* Уменьшаем размер шрифта для подписчиков */ - margin-bottom: 5px; - font-weight: 500; /* Выделяем имя пользователя */ -} - -.subscribers { - font-size: 15px; /* Уменьшаем размер шрифта для подписчиков */ - color: #666; /* Делаем текст подписчиков менее заметным */ -} - -a { - display: flex; - align-items: center; -} - -.a img { - border-radius: 0%; -} - - -#subscribe-button { - background-color: #bdbdbdf9; - border-radius: 50px; - width: 140px; - height: 30px; - border: none; -} - -#subscribe-button:hover { - background-color: #808080; - color: #fff; -} \ No newline at end of file diff --git a/src/static/upload_page.css b/src/static/upload_page.css deleted file mode 100644 index 80ebb7a..0000000 --- a/src/static/upload_page.css +++ /dev/null @@ -1,288 +0,0 @@ -body { - font-family: 'Roboto', sans-serif; - background-color: #f9f9f9; - margin: 0; - padding: 0; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - background-color: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -header .search-container { - display: flex; - align-items: center; - vertical-align: center; - margin: 0 auto; /* Центрируем контейнер */ -} - -header .search-container input[type="text"] { - padding: 8px; - width: 400px; - vertical-align: center; - border: 1px solid #ccc; - border-radius: 5px 0 0 5px; - font-size: 13px; -} - -header .search-container button { - background-color: #ffffff; - border: 1px solid #ccc; - vertical-align: center; - border-radius: 0 5px 5px 0; - padding: 8px 12px; - cursor: pointer; -} - -header h1 { - margin: 0; - font-size: 24px; - color: #333; -} - -header nav { - display: flex; - align-items: center; -} - -header nav a { - display: flex; - align-items: center; - text-decoration: none; - color: inherit; - margin-left: 15px; -} - -header nav a img { - border-radius: 50%; - width: 30px; - height: 30px; -} - -.channel-container { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 20px; -} - -.channel-container h2 { - margin-bottom: 10px; - font-size: 22px; - color: #333; -} - -.upload-form { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 20px; -} - -.upload-button, .submit-button { - display: inline-flex; - align-items: center; - padding: 10px 20px; - margin: 10px 0; - font-size: 16px; - font-weight: bold; - color: #fff; - background-color: #4CAF50; /* Зеленый цвет */ - border: none; - border-radius: 4px; /* Более скругленные углы */ - cursor: pointer; - text-align: center; - text-decoration: none; - transition: background-color 0.3s ease, box-shadow 0.3s ease; /* Добавляем transition для box-shadow */ - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); /* Тень */ -} - -.upload-button:hover, .submit-button:hover { - background-color: #45a049; /* Более темный зеленый при наведении */ - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); /* Увеличиваем тень при наведении */ -} - -.upload-button i, .submit-button i { - margin-right: 5px; - font-size: 18px; -} - -.upload-button:hover, .submit-button:hover { - background-color: #0056b3; -} - -.upload-button i, .submit-button i { - margin-right: 5px; - font-size: 18px; -} - -.video-container { - display: flex; - flex-wrap: wrap; - justify-content: center; - padding: 20px; -} - -.video-preview { - width: 320px; - margin: 15px; - border-radius: 10px; - overflow: hidden; - background-color: #ffffff00; - transition: transform 0.3s, box-shadow 0.3s; - padding: 5px; -} - -header a:link, header a:visited { - color: inherit; - text-decoration: none; -} - -header a:hover, header a:active { - color: inherit; - text-decoration: none; -} - -.video-preview:hover { - transform: scale(1.0); - background-color: #fff; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); -} - -.video-preview img { - width: 100%; - height: auto; - object-fit: cover; - border-radius: 10px; - transition: transform 0.3s; -} - -.video-preview:hover img { - transform: scale(1.01); -} - -.video-preview h3 { - margin: 10px 0 5px; - font-size: 16px; - color: #333; - display: flex; - align-items: center; -} - -.video-preview h3 img { - width: 30px; - height: 30px; - object-fit: cover; - border-radius: 50%; - margin-right: 10px; -} - -.video-preview p { - margin: 0; - font-size: 14px; - color: #666; -} - -.subscribe-button { - background-color: #ff0000; /* Синий цвет */ - color: white; - border: none; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 12px; - border-radius: 5px; /* Скругленные углы */ - cursor: pointer; -} - -.unsubscribe-button { - background-color: #3e3e3e; /* Красный цвет */ - color: white; - border: none; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 12px; - border-radius: 5px; /* Скругленные углы */ - cursor: pointer; -} - -h1 { - color: #333; -} -main { - display: flex; - justify-content: center; - align-items: center; - min-height: calc(100vh - 250px); /* Высота окна браузера минус высота header */ - padding-top: 10px; /* Отступ сверху */ -} -form { - width: 100%; - max-width: 500px; - padding: 30px; - background-color: #fff; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -input[type="text"], -textarea { - width: 100%; - padding: 12px; - margin-bottom: 15px; - border: 1px solid #ddd; - border-radius: 4px; - box-sizing: border-box; -} - -input[type="file"] { - margin-bottom: 15px; -} - -button[type="submit"] { - padding: 12px 20px; - background-color: #448aff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; -} - -button[type="submit"]:hover { - background-color: #3367d6; -} - -.loading-animation { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.loading-spinner { - width: 60px; - height: 60px; - border: 5px solid #ccc; - border-radius: 50%; - border-top-color: #007bff; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} \ No newline at end of file diff --git a/src/static/video_page.css b/src/static/video_page.css deleted file mode 100644 index e841ddd..0000000 --- a/src/static/video_page.css +++ /dev/null @@ -1,266 +0,0 @@ -/* src/static/video_page.css */ -body { - font-family: 'Moderustic', sans-serif; - background-color: #fff; - margin: 0; - padding: 0; -} - -header { - display: flex; - flex: 1; - justify-content: space-between; - align-items: center; - padding: 20px; - background-color: #fff; -} - - -header h1 { - margin: 0; - font-size: 24px; - color: #333; -} - -header a { - display: flex; - align-items: center; - text-decoration: none; - color: inherit; -} - -header a:link, header a:visited { - color: inherit; - text-decoration: none; -} - -header a:hover, header a:active { - color: inherit; - text-decoration: none; -} - -header img { - border-radius: 50%; - width: 30px; - height: 30px; -} - -.main-container { - display: flex; - padding: 20px; -} - -.video-section { - flex: 2; - margin-right: 20px; - margin-left: 10px; /* Отступ справа от левого края экрана */ - margin-top: 10px; /* Отступ снизу от шапки */ -} - -.video-info { - /* background-color: #fff; */ - /* padding: 20px; */ - border-radius: 10px; - /* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); */ - max-width: 1280px; -} - -.video-info video { - width: 100%; - height: 100%; - max-width: 1280px; - max-height: 720px; - border-radius: 10px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.video-info h2 { - margin: 20px 0 10px; - font-size: 24px; - color: #333; -} - -.author-info { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.author-info img { - border-radius: 50%; - margin-right: 10px; -} - -.author-info span { - font-size: 16px; - color: #333; - margin-right: 20px; - align-self: center; /* Центрирование тега автора по вертикали */ -} - -.likes-dislikes { - display: flex; - align-items: center; - /* gap: 10px; */ - width: 150px; - height: 30px; - border-radius: 50px; -} - -.likes-dislikes button { - background-color: transparent; - border: none; - cursor: pointer; - display: flex; - align-items: center; - font-size: 16px; - color: #333; - height: 30px; -} - -.description { - color: #000000; - background-color: #bdbdbd9f; - padding: 10px; - border-radius: 10px; - font-size: 16px; - max-height: 60px; - overflow: hidden; - transition: max-height 0.3s; -} - -.description.expanded { - max-height: none; -} - -.show-more { - color: #000000; - font-weight: 770; - background-color: transparent; - border: none; - cursor: pointer; -} - -.related-videos { - width: auto; - height: auto; - padding: 20px; - margin-top: 10px; -} - -.related-videos h3 { - font-size: 20px; - color: #333; - margin-bottom: 10px; -} - -.related-videos ul { - list-style: none; - padding: 0; -} - -.related-videos ul li { - display: flex; - align-items: flex-start; - margin-bottom: 10px; - color: inherit; -} - -.related-videos ul li * { - margin-top: 0; - vertical-align: top; - color: inherit; -} - -.related-videos ul li div { - display: flex; - flex-direction: column; -} - -.related-videos ul li img { - width: 120px; - height: 70px; - border-radius: 5px; - margin-right: 10px; -} - -.related-videos ul li a { - font-size: 16px; - color: inherit; - text-decoration: none; -} - -.related-videos ul li a:hover { - text-decoration: underline; - color: inherit; -} - -/* video_page.css или channel_page.css */ -.related-videos, .related-videos-mobile { - position: relative; /* Для абсолютного позиционирования */ - right: 0; /* Привязываем к правой границе */ - width: 25%; /* Ширина 25% */ - top: 0px; /* Позиционируем ниже видеоплеера */ - max-height: 80%; /* Максимальная высота */ - overflow-y: auto; /* Добавляем вертикальную прокрутку */ - display: none; /* Скрываем по умолчанию */ - z-index: 1; /* Устанавливаем z-index для наложения */ -} - -.video-section { - width: 75%; /* Ограничиваем ширину видео */ - position: relative; /* Устанавливаем позиционирование для видео */ -} - -@media (max-width: 1080px) { - .related-videos { - display: block; /* Отображаем предложенные видео на экранах меньше 800px */ - } - - .video-section { - width: calc(100% - 25%); /* Изменяем ширину видео */ - } -} - -a { - text-decoration: none; /* Убираем подчеркивания у всех ссылок */ - color: inherit; -} -a:link, a:visited, a:hover, a:active { - text-align: left; - color: inherit; - text-decoration: none; -} - -.vert-separator-description { - height: 30px; /* Высота сепаратора */ - width: 1px; /* Толщина линии */ - background-color: #ccc; /* Цвет линии */ - /* margin: 15px auto; Отступы сверху/снизу, центрирование по горизонтали */ -} - -.comments { - color: #000000; - padding: 10px; - border-radius: 10px; - font-size: 16px; -} - -.comment-item { - display: flex; - align-items: flex-start; - margin-bottom: 10px; -} - -.comment-item img { - margin-right: 10px; -} - -.comment-content { - flex: 1; /* Занимаем оставшееся пространство */ -} - -.comment-content a { - text-decoration: none; - color: rgb(0, 0, 0); -} \ No newline at end of file diff --git a/src/templates/base.html b/src/templates/base.html deleted file mode 100644 index 14a1536..0000000 --- a/src/templates/base.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - NeTube - - - - -
-

NeTube

- -
- - - - - \ No newline at end of file diff --git a/src/templates/channel.html b/src/templates/channel.html deleted file mode 100644 index b812aba..0000000 --- a/src/templates/channel.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - {{ user.username }} Channel - - - - - -
-

NeTube

- -
- - - -
-

@{{ Markup(user.username) }}

-

{{ user.subscribers.count() }} подписчиков

- - {% if current_user and current_user.username != user.username %} - - {% endif %} - -
- {% for video in videos %} - - {% endfor %} -
-
- - - - \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html deleted file mode 100644 index df780cb..0000000 --- a/src/templates/index.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - NeTube - - - - - - - - -
-

- NeTube - by Nazaryan ART -

-
- - -
- - -
- - - - {% if search_results %} -

Результаты поиска:

-
- {% for video in search_results %} - - {% endfor %} -
- {% else %} -
- {% for video in videos %} - - {% endfor %} -
- {% endif %} - - - - - \ No newline at end of file diff --git a/src/templates/login.html b/src/templates/login.html deleted file mode 100644 index 17c5f29..0000000 --- a/src/templates/login.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Вход - - - - - -

Вход

- {% if error %} -

{{ error }}

- {% endif %} -
- - - - -

Нет аккаунта? Зарегистрируйтесь

-
- - \ No newline at end of file diff --git a/src/templates/register.html b/src/templates/register.html deleted file mode 100644 index 3fc93a1..0000000 --- a/src/templates/register.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Регистрация - - - - -

Регистрация

- {% if error %} -

{{ error }}

- {% endif %} -
- - - - - -

Уже есть аккаунт? Войдите

-
- - \ No newline at end of file diff --git a/src/templates/upload.html b/src/templates/upload.html deleted file mode 100644 index 76e25d3..0000000 --- a/src/templates/upload.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - Загрузить видео - - - -
-

NeTube

- -
- -
-
-

Загрузить видео

- - - - - - -

0 / 500

- - - - - -
-
- - - - - - - - - \ No newline at end of file diff --git a/src/templates/video.html b/src/templates/video.html deleted file mode 100644 index ce0a58d..0000000 --- a/src/templates/video.html +++ /dev/null @@ -1,258 +0,0 @@ - - - - - - {{ video.title }} - - - - - - -
-

NeTube

- -
- - - -
-
-
- -

{{ video.title }}

-
- - Аватар автора - @{{ Markup(video.author.username) }} - {{ video.author.subscribers.count() }} подписчиков - - - - {% if current_user != video.author %} - - {% endif %} - -

- - {{ video.views }} views

- - - -
-

{{ video.description }}

- -
- -
- - -
- - -

Комментарии

- -
-
-
- - -
- - - - \ No newline at end of file diff --git a/src/static/avatars/changeuser.png b/static/avatars/changeuser.png similarity index 100% rename from src/static/avatars/changeuser.png rename to static/avatars/changeuser.png diff --git a/src/static/avatars/dislike.png b/static/avatars/dislike.png similarity index 100% rename from src/static/avatars/dislike.png rename to static/avatars/dislike.png diff --git a/src/static/avatars/exit.png b/static/avatars/exit.png similarity index 100% rename from src/static/avatars/exit.png rename to static/avatars/exit.png diff --git a/src/static/avatars/like.png b/static/avatars/like.png similarity index 100% rename from src/static/avatars/like.png rename to static/avatars/like.png diff --git a/src/static/avatars/log-out.png b/static/avatars/log-out.png similarity index 100% rename from src/static/avatars/log-out.png rename to static/avatars/log-out.png diff --git a/src/static/avatars/persdata.png b/static/avatars/persdata.png similarity index 100% rename from src/static/avatars/persdata.png rename to static/avatars/persdata.png diff --git a/src/static/avatars/plus.png b/static/avatars/plus.png similarity index 100% rename from src/static/avatars/plus.png rename to static/avatars/plus.png diff --git a/src/static/avatars/question.png b/static/avatars/question.png similarity index 100% rename from src/static/avatars/question.png rename to static/avatars/question.png diff --git a/src/static/avatars/switchacc.png b/static/avatars/switchacc.png similarity index 100% rename from src/static/avatars/switchacc.png rename to static/avatars/switchacc.png diff --git a/src/static/avatars/upload.png b/static/avatars/upload.png similarity index 100% rename from src/static/avatars/upload.png rename to static/avatars/upload.png diff --git a/static/avatars/users/Listex2_avatar.jpg b/static/avatars/users/Listex2_avatar.jpg new file mode 100644 index 0000000..736667e Binary files /dev/null and b/static/avatars/users/Listex2_avatar.jpg differ diff --git a/static/avatars/users/Listex_avatar.jpg b/static/avatars/users/Listex_avatar.jpg new file mode 100644 index 0000000..736667e Binary files /dev/null and b/static/avatars/users/Listex_avatar.jpg differ diff --git a/static/avatars/users/Listex_avatar.png b/static/avatars/users/Listex_avatar.png new file mode 100644 index 0000000..f398bbb Binary files /dev/null and b/static/avatars/users/Listex_avatar.png differ diff --git a/src/static/avatars/users/Slidee36_avatar.png b/static/avatars/users/Slidee36_avatar.png similarity index 100% rename from src/static/avatars/users/Slidee36_avatar.png rename to static/avatars/users/Slidee36_avatar.png diff --git a/src/static/avatars/users/plus.png b/static/avatars/users/plus.png similarity index 100% rename from src/static/avatars/users/plus.png rename to static/avatars/users/plus.png diff --git a/src/static/avatars/users/question.png b/static/avatars/users/question.png similarity index 100% rename from src/static/avatars/users/question.png rename to static/avatars/users/question.png diff --git a/static/css/video_player.css b/static/css/video_player.css new file mode 100644 index 0000000..19e493e --- /dev/null +++ b/static/css/video_player.css @@ -0,0 +1,41 @@ +#settingsMenu { + border: 1px solid rgba(255, 255, 255, 0.2); +} +/* Контейнер плеера */ +#videoPlayerContainer { + position: relative; + width: 100%; + height: 600px; /* Высота для обычного режима */ + background: black; /* Черный фон для черных полос */ + overflow: hidden; +} + +/* Видео */ +#NeTubeVideoPlayer { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + object-fit: contain; /* Сохранение соотношения сторон */ +} + +/* Полноэкранный режим */ +#videoPlayerContainer.fullscreen { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; /* Высота во весь экран */ + z-index: 9999; /* Положение поверх других элементов */ + margin: 0; + padding: 0; + background: black; +} + +#NeTubeVideoPlayer.fullscreen { + width: 100%; + height: 100%; + object-fit: contain; /* Сохранение соотношения сторон в полноэкранном режиме */ +} diff --git a/static/js/subscribe_handler.js b/static/js/subscribe_handler.js new file mode 100644 index 0000000..67c5f30 --- /dev/null +++ b/static/js/subscribe_handler.js @@ -0,0 +1,35 @@ +document.querySelector('#subscribe-button').addEventListener('click', function() { + const userId = this.dataset.userId; + const isSubscribed = this.innerText === 'Отписаться'; + + fetch(isSubscribed ? `/api/unsubscribe/${userId}` : `/api/subscribe/${userId}`, { + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + // Обновляем текст кнопки и ее цвет в зависимости от состояния подписки + this.innerText = data.subscribed ? 'Отписаться' : 'Подписаться'; + + // Обновляем цвет кнопки + if (data.subscribed) { + this.classList.add('bg-gray-200', 'text-gray-600', 'border-gray-300'); + this.classList.remove('bg-red-500', 'text-white', 'border-red-500'); + } else { + this.classList.add('bg-red-500', 'text-white', 'border-red-500'); + this.classList.remove('bg-gray-200', 'text-gray-600', 'border-gray-300'); + } + + // Обновляем количество подписчиков + const subscribersSpans = document.querySelectorAll('.subscribers'); + subscribersSpans.forEach(span => { + span.innerText = data.subscribers + ' подписчиков'; + }); + } else { + console.error(data.message); + } + }) + .catch(error => { + console.error('Ошибка:', error); + }); +}); diff --git a/static/js/video_player.js b/static/js/video_player.js new file mode 100644 index 0000000..c098d51 --- /dev/null +++ b/static/js/video_player.js @@ -0,0 +1,124 @@ +// Get references to DOM elements +const video = document.getElementById('NeTubeVideoPlayer'); +const playPauseButton = document.getElementById('playPause'); +const muteUnmuteButton = document.getElementById('muteUnmute'); +const fullscreenButton = document.getElementById('fullscreen'); +const videoPlayerContainer = document.getElementById('videoPlayerContainer'); +const seekBar = document.getElementById('seekBar'); +const volumeSlider = document.getElementById('volume'); +const volumeText = document.getElementById('volumeText'); +const currentTimeDisplay = document.getElementById('currentTime'); +const durationDisplay = document.getElementById('duration'); +const pipButton = document.getElementById('pip'); +const settingsButton = document.getElementById('settings'); +const settingsMenu = document.getElementById('settingsMenu'); +const speedSelect = document.getElementById('speed'); +const autoplayCheckbox = document.getElementById('autoplay'); + +// Play or pause video +const updatePlayPauseButton = () => { + playPauseButton.innerHTML = video.paused ? '' : ''; +}; + +playPauseButton.addEventListener('click', () => { + video.paused ? video.play() : video.pause(); +}); + + +video.addEventListener('click', () => { + if (video.paused) { + video.play(); + playPauseButton.innerHTML = ''; + } else { + video.pause(); + playPauseButton.innerHTML = ''; + } +}); + + +video.addEventListener('play', updatePlayPauseButton); +video.addEventListener('pause', updatePlayPauseButton); + +// Mute or unmute video +muteUnmuteButton.addEventListener('click', () => { + video.muted = !video.muted; + muteUnmuteButton.innerHTML = video.muted ? '' : ''; + volumeSlider.value = video.muted ? 0 : video.volume; + volumeText.innerText = Math.round(video.volume * 100); +}); + +// Fullscreen mode +fullscreenButton.addEventListener('click', () => { + if (!document.fullscreenElement) { + videoPlayerContainer.requestFullscreen().catch(err => { + console.log("Error attempting to enable full-screen mode:", err); + }); + fullscreenButton.innerHTML = ''; // Иконка для выхода из полноэкранного режима + } else { + document.exitFullscreen(); + fullscreenButton.innerHTML = ''; // Иконка для входа в полноэкранный режим + } +}); + +document.addEventListener('fullscreenchange', () => { + if (document.fullscreenElement) { + videoPlayerContainer.classList.add('fullscreen'); + document.body.style.overflow = 'hidden'; // Предотвращение прокрутки + } else { + videoPlayerContainer.classList.remove('fullscreen'); + document.body.style.overflow = ''; // Восстановление прокрутки + } +}); +// Picture-in-Picture mode +pipButton.addEventListener('click', async () => { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + } else if (video.requestPictureInPicture) { + await video.requestPictureInPicture(); + } +}); + +// Toggle visibility of settings menu +settingsButton.addEventListener('click', () => { + settingsMenu.classList.toggle('hidden'); +}); + +// Change playback speed +speedSelect.addEventListener('change', (event) => { + video.playbackRate = parseFloat(event.target.value); +}); + +// Toggle autoplay +autoplayCheckbox.addEventListener('change', (event) => { + video.loop = event.target.checked; +}); + +// Update seek bar and current time +video.addEventListener('timeupdate', () => { + seekBar.value = (video.currentTime / video.duration) * 100; + currentTimeDisplay.textContent = formatTime(video.currentTime); +}); + +// Seek video when seek bar changes +seekBar.addEventListener('input', () => { + video.currentTime = (seekBar.value / 100) * video.duration; +}); + +// Update volume +volumeSlider.addEventListener('input', () => { + video.volume = volumeSlider.value; + muteUnmuteButton.innerHTML = video.volume === 0 ? '' : ''; + volumeText.innerText = Math.round(video.volume * 100); +}); + +// Display video duration +video.addEventListener('loadedmetadata', () => { + durationDisplay.textContent = formatTime(video.duration); +}); + +// Format time in MM:SS +function formatTime(time) { + const minutes = Math.floor(time / 60).toString().padStart(2, '0'); + const seconds = Math.floor(time % 60).toString().padStart(2, '0'); + return `${minutes}:${seconds}`; +} diff --git a/static/thumbnails/3agjcnYUdH.jpg b/static/thumbnails/3agjcnYUdH.jpg new file mode 100644 index 0000000..0947303 Binary files /dev/null and b/static/thumbnails/3agjcnYUdH.jpg differ diff --git a/src/static/thumbnails/R8OCDOIyLO.jpg b/static/thumbnails/R8OCDOIyLO.jpg similarity index 100% rename from src/static/thumbnails/R8OCDOIyLO.jpg rename to static/thumbnails/R8OCDOIyLO.jpg diff --git a/src/static/thumbnails/h4uZGGbqMw.jpg b/static/thumbnails/h4uZGGbqMw.jpg similarity index 100% rename from src/static/thumbnails/h4uZGGbqMw.jpg rename to static/thumbnails/h4uZGGbqMw.jpg diff --git a/src/static/thumbnails/ppieSeyay3.jpg b/static/thumbnails/ppieSeyay3.jpg similarity index 100% rename from src/static/thumbnails/ppieSeyay3.jpg rename to static/thumbnails/ppieSeyay3.jpg diff --git a/static/videos/0P5xmwY0TP.mp4 b/static/videos/0P5xmwY0TP.mp4 new file mode 100644 index 0000000..40ad26f Binary files /dev/null and b/static/videos/0P5xmwY0TP.mp4 differ diff --git a/static/videos/3agjcnYUdH.mp4 b/static/videos/3agjcnYUdH.mp4 new file mode 100644 index 0000000..40ad26f Binary files /dev/null and b/static/videos/3agjcnYUdH.mp4 differ diff --git a/src/static/videos/R8OCDOIyLO.mp4 b/static/videos/R8OCDOIyLO.mp4 similarity index 100% rename from src/static/videos/R8OCDOIyLO.mp4 rename to static/videos/R8OCDOIyLO.mp4 diff --git a/static/videos/SyjpHkA0mC.mp4 b/static/videos/SyjpHkA0mC.mp4 new file mode 100644 index 0000000..40ad26f Binary files /dev/null and b/static/videos/SyjpHkA0mC.mp4 differ diff --git a/static/videos/bOnvU5h2pO.mp4 b/static/videos/bOnvU5h2pO.mp4 new file mode 100644 index 0000000..40ad26f Binary files /dev/null and b/static/videos/bOnvU5h2pO.mp4 differ diff --git a/static/videos/eNdSoHYu31.mp4 b/static/videos/eNdSoHYu31.mp4 new file mode 100644 index 0000000..40ad26f Binary files /dev/null and b/static/videos/eNdSoHYu31.mp4 differ diff --git a/static/videos/hRHe8uHVrJ.mp4 b/static/videos/hRHe8uHVrJ.mp4 new file mode 100644 index 0000000..716592b Binary files /dev/null and b/static/videos/hRHe8uHVrJ.mp4 differ diff --git a/static/videos/jMvIoNB1f3.mp4 b/static/videos/jMvIoNB1f3.mp4 new file mode 100644 index 0000000..40ad26f Binary files /dev/null and b/static/videos/jMvIoNB1f3.mp4 differ diff --git a/src/templates/admin_login.html b/templates/admin/admin_login.html similarity index 97% rename from src/templates/admin_login.html rename to templates/admin/admin_login.html index efabc87..75ebaf2 100644 --- a/src/templates/admin_login.html +++ b/templates/admin/admin_login.html @@ -1,19 +1,19 @@ - - - - - Вход в панель администрирования - - - - - - -

Вход в панель администрирования

-
- - - -
- + + + + + Вход в панель администрирования + + + + + + +

Вход в панель администрирования

+
+ + + +
+ \ No newline at end of file diff --git a/src/templates/administration.html b/templates/admin/administration.html similarity index 97% rename from src/templates/administration.html rename to templates/admin/administration.html index 5e5fd63..4d55d1a 100644 --- a/src/templates/administration.html +++ b/templates/admin/administration.html @@ -1,34 +1,34 @@ - - - - - Страница администрирования - - - - - - -

Панель администрирования

- -

Пользователи:

- - -

Видео:

- - + + + + + Страница администрирования + + + + + + +

Панель администрирования

+ +

Пользователи:

+ + +

Видео:

+ + \ No newline at end of file diff --git a/src/templates/edit_user.html b/templates/admin/edit_user.html similarity index 97% rename from src/templates/edit_user.html rename to templates/admin/edit_user.html index 59c8a84..d024b78 100644 --- a/src/templates/edit_user.html +++ b/templates/admin/edit_user.html @@ -1,58 +1,58 @@ - - - - - Редактировать пользователя - - - - - - -

Редактировать пользователя {{ user.username }}

- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
    - {% for category, message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} - -
- - - - - - - - - -

Подписчики:

-
    - {% for subscriber in user.subscribers %} -
  • {{ subscriber.username }} - -
  • - {% endfor %} -
- - - - -

Видео:

-
    - {% for video in user.videos %} -
  • {{ video.title }} - -
  • - {% endfor %} -
- - -
- + + + + + Редактировать пользователя + + + + + + +

Редактировать пользователя {{ user.username }}

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ + + + + + + + + +

Подписчики:

+
    + {% for subscriber in user.subscribers %} +
  • {{ subscriber.username }} + +
  • + {% endfor %} +
+ + + + +

Видео:

+
    + {% for video in user.videos %} +
  • {{ video.title }} + +
  • + {% endfor %} +
+ + +
+ \ No newline at end of file diff --git a/src/templates/edit_video.html b/templates/admin/edit_video.html similarity index 97% rename from src/templates/edit_video.html rename to templates/admin/edit_video.html index a486771..1b74754 100644 --- a/src/templates/edit_video.html +++ b/templates/admin/edit_video.html @@ -1,34 +1,34 @@ - - - - - Редактировать видео - - - - - - -

Редактировать видео {{ video.title }}

- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
    - {% for category, message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} - -
- - - - - - - -
- + + + + + Редактировать видео + + + + + + +

Редактировать видео {{ video.title }}

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ + + + + + + +
+ \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..ba0062b --- /dev/null +++ b/templates/base.html @@ -0,0 +1,145 @@ + + + + + + {% block title %}NeTube{% endblock %} + + + + + + +
+
+

+ NeTube +

+
+
+ + +
+
+ +
+
+ +
+ + + + + + {% block content %}{% endblock %} +
+ + + + diff --git a/templates/channel/channel.html b/templates/channel/channel.html new file mode 100644 index 0000000..70b643a --- /dev/null +++ b/templates/channel/channel.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% block content %} +
+ + +
+ + +
+

@{{ user.username }}

+

{{ user.subscribers.count() }} подписчиков

+ + + {% if current_user and current_user.username != user.username %} + + {% endif %} +
+ + + +
+
+ +{% endblock %} diff --git a/templates/errors/400.html b/templates/errors/400.html new file mode 100644 index 0000000..35e299b --- /dev/null +++ b/templates/errors/400.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Ошибка 400 - NeTube{% endblock %} + +{% block content %} +
+
+

400

+

Неверный запрос

+

Кажется, вы отправили неверный запрос. Пожалуйста, проверьте введенные данные и попробуйте снова.

+ На главную +
+
+{% endblock %} diff --git a/templates/errors/401.html b/templates/errors/401.html new file mode 100644 index 0000000..a586991 --- /dev/null +++ b/templates/errors/401.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Ошибка 401 - NeTube{% endblock %} + +{% block content %} +
+
+

401

+

Неавторизован

+

Для доступа к этой странице требуется авторизация. Пожалуйста, войдите в систему или зарегистрируйтесь.

+ Войти +
+
+{% endblock %} diff --git a/templates/errors/403.html b/templates/errors/403.html new file mode 100644 index 0000000..604e890 --- /dev/null +++ b/templates/errors/403.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Ошибка 403 - NeTube{% endblock %} + +{% block content %} +
+
+

403

+

Доступ запрещен

+

У вас нет прав для доступа к этой странице. Пожалуйста, свяжитесь с администратором, если считаете, что это ошибка.

+ На главную +
+
+{% endblock %} diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 0000000..14dee89 --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Ошибка 404 - NeTube{% endblock %} + +{% block content %} +
+
+

404

+

Страница не найдена

+

Извините, но страница, которую вы ищете, не найдена. Проверьте URL или вернитесь на главную страницу.

+ На главную +
+
+{% endblock %} diff --git a/templates/errors/500.html b/templates/errors/500.html new file mode 100644 index 0000000..2baac46 --- /dev/null +++ b/templates/errors/500.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} + +{% block title %}Ошибка 500 - NeTube{% endblock %} + +{% block content %} +
+
+

500

+

Внутренняя ошибка сервера

+

Произошла ошибка на сервере. Мы уже работаем над её исправлением. Пожалуйста, попробуйте позже.

+ {% if error %} +
+

Детали ошибки:

+
{{ error }}
+
+ {% endif %} + На главную +
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a49459d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} + +{% block title %}Главная - NeTube{% endblock %} + +{% block content %} +{% if search_results %} +

Результаты поиска: {{ search_query }}

+
+ {% for video in search_results %} +
+ + + {{ video.title }} + + + +
+ + +

+ {{ video.views }} просмотров • {{ video.likes }} лайков +

+
+
+ {% endfor %} +
+{% else %} + +{% endif %} +{% endblock %} diff --git a/templates/user/login.html b/templates/user/login.html new file mode 100644 index 0000000..59cee6b --- /dev/null +++ b/templates/user/login.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Вход

+ + {% if error %} +

{{ error }}

+ {% endif %} + +
+ + + +
+ +

Нет аккаунта? Зарегистрируйтесь

+
+
+{% endblock %} diff --git a/templates/user/register.html b/templates/user/register.html new file mode 100644 index 0000000..5bc1199 --- /dev/null +++ b/templates/user/register.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Регистрация

+ + {% if error %} +

{{ error }}

+ {% endif %} + +
+ + + +
+ +

Уже есть аккаунт? Войдите

+
+
+{% endblock %} diff --git a/templates/video/upload.html b/templates/video/upload.html new file mode 100644 index 0000000..e105b6d --- /dev/null +++ b/templates/video/upload.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} + +{% block title %}Загрузка видео - NeTube{% endblock %} + +{% block content %} + + +
+
+

Загрузить видео

+ + + + + + +

0 / 500

+ + + + + + + + +
+
+ + + + + +{% endblock %} diff --git a/templates/video/video.html b/templates/video/video.html new file mode 100644 index 0000000..ff3d66e --- /dev/null +++ b/templates/video/video.html @@ -0,0 +1,362 @@ +{% extends 'base.html' %} + +{% block title %}{{ video.title }} - NeTube{% endblock %} + +{% block content %} + +
+
+ +
+ +
+ + + +
+
+ + + + + + + +
+ + 100 +
+ + + + 00:00 + / + 00:00 +
+ +
+ + + + +
+ + + + +
+ + + + + +
+
+
+ + +

{{ video.title }}

+ + +
+
+ + Аватар автора +
+@{{ video.author.username }} +{{ video.author.subscribers.count() }} подписчиков + +
+
+ + +{% if current_user != video.author %} + +{% endif %} + + +
+ +
+

{{ video.views }} просмотров

+ + +
+ + +
+ +
+
+ + +
+

+ {{ video.description[:200] }}... + +

+ +
+ + +
+

Комментарии

+
+ + +
+ + +
+ +
+ + +
+

Другие видео

+ +
+
+
+ + + + + +{% endblock %}