From 6f449eea4e73258dea1685224bbec6aa909866a2 Mon Sep 17 00:00:00 2001 From: ThisBirchWood Date: Fri, 28 Feb 2025 19:38:03 +0000 Subject: [PATCH 1/6] REFACTOR: Moved streaming tasks to dedicated folder and updated refs --- web_server/blueprints/streams.py | 3 +- web_server/celery_tasks/__init__.py | 56 ---------------------- web_server/celery_tasks/streaming.py | 71 ++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 58 deletions(-) create mode 100644 web_server/celery_tasks/streaming.py diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 854ce78..5e40ab8 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -5,7 +5,7 @@ from utils.user_utils import get_user_id from blueprints.middleware import login_required from database.database import Database from datetime import datetime -from celery_tasks import update_thumbnail, combine_ts_stream +from celery_tasks.streaming import update_thumbnail, combine_ts_stream from dateutil import parser from utils.path_manager import PathManager import json @@ -205,7 +205,6 @@ def publish_stream(): periodically update thumbnail """ - try: data = json.loads(request.form.get("data")) except json.JSONDecodeError as ex: diff --git a/web_server/celery_tasks/__init__.py b/web_server/celery_tasks/__init__.py index c119113..9ea551f 100644 --- a/web_server/celery_tasks/__init__.py +++ b/web_server/celery_tasks/__init__.py @@ -1,10 +1,4 @@ from celery import Celery, shared_task, Task -from utils.stream_utils import generate_thumbnail, get_streamer_live_status -from time import sleep -from os import listdir, remove -from datetime import datetime -from celery_tasks.preferences import user_preferences -import subprocess def celery_init_app(app) -> Celery: class FlaskTask(Task): @@ -23,53 +17,3 @@ def celery_init_app(app) -> Celery: celery_app.set_default() app.extensions["celery"] = celery_app return celery_app - - -@shared_task -def update_thumbnail(user_id, stream_file, thumbnail_file, sleep_time) -> None: - """ - Updates the thumbnail of a stream periodically - """ - - if get_streamer_live_status(user_id)['is_live']: - print("Updating thumbnail...") - generate_thumbnail(stream_file, thumbnail_file) - update_thumbnail.apply_async((user_id, stream_file, thumbnail_file, sleep_time), countdown=sleep_time) - else: - print("Stream has ended, stopping thumbnail updates") - -@shared_task -def combine_ts_stream(stream_path, vods_path, vod_file_name): - """ - Combines all ts files into a single vod, and removes the ts files - """ - ts_files = [f for f in listdir(stream_path) if f.endswith(".ts")] - ts_files.sort() - - # Create temp file listing all ts files - with open(f"{stream_path}/list.txt", "w") as f: - for ts_file in ts_files: - f.write(f"file '{ts_file}'\n") - - # Concatenate all ts files into a single vod - - vod_command = [ - "ffmpeg", - "-f", - "concat", - "-safe", - "0", - "-i", - f"{stream_path}/list.txt", - "-c", - "copy", - f"{vods_path}/{vod_file_name}.mp4" - ] - - subprocess.run(vod_command) - - # Remove ts files - for ts_file in ts_files: - remove(f"{stream_path}/{ts_file}") - # Remove m3u8 file - remove(f"{stream_path}/index.m3u8") \ No newline at end of file diff --git a/web_server/celery_tasks/streaming.py b/web_server/celery_tasks/streaming.py new file mode 100644 index 0000000..17aff54 --- /dev/null +++ b/web_server/celery_tasks/streaming.py @@ -0,0 +1,71 @@ +from celery import Celery, shared_task, Task +from datetime import datetime +from celery_tasks.preferences import user_preferences +from utils.stream_utils import generate_thumbnail, get_streamer_live_status +from time import sleep +from os import listdir, remove +import subprocess + +@shared_task +def update_thumbnail(user_id, stream_file, thumbnail_file, sleep_time) -> None: + """ + Updates the thumbnail of a stream periodically + """ + + if get_streamer_live_status(user_id)['is_live']: + print("Updating thumbnail...") + generate_thumbnail(stream_file, thumbnail_file) + update_thumbnail.apply_async((user_id, stream_file, thumbnail_file, sleep_time), countdown=sleep_time) + else: + print("Stream has ended, stopping thumbnail updates") + +@shared_task +def combine_ts_stream(stream_path, vods_path, vod_file_name): + """ + Combines all ts files into a single vod, and removes the ts files + """ + ts_files = [f for f in listdir(stream_path) if f.endswith(".ts")] + ts_files.sort() + + # Create temp file listing all ts files + with open(f"{stream_path}/list.txt", "w") as f: + for ts_file in ts_files: + f.write(f"file '{ts_file}'\n") + + # Concatenate all ts files into a single vod + + vod_command = [ + "ffmpeg", + "-f", + "concat", + "-safe", + "0", + "-i", + f"{stream_path}/list.txt", + "-c", + "copy", + f"{vods_path}/{vod_file_name}.mp4" + ] + + subprocess.run(vod_command) + + # Remove ts files + for ts_file in ts_files: + remove(f"{stream_path}/{ts_file}") + # Remove m3u8 file + remove(f"{stream_path}/index.m3u8") + +@shared_task +def convert_image_to_png(image_path, png_path): + """ + Converts an image to a png + """ + image_command = [ + "ffmpeg", + "-y", + "-i", + image_path, + png_path + ] + + subprocess.run(image_command) \ No newline at end of file From bd3b39da8950e6fb0c10a2b0db793c481a0fb166 Mon Sep 17 00:00:00 2001 From: ThisBirchWood Date: Fri, 28 Feb 2025 20:15:21 +0000 Subject: [PATCH 2/6] REFACTOR: Path manager now creates directories --- web_server/utils/path_manager.py | 45 +++++++++++++++++++++++++++----- web_server/utils/stream_utils.py | 2 +- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/web_server/utils/path_manager.py b/web_server/utils/path_manager.py index 6799337..2c849a9 100644 --- a/web_server/utils/path_manager.py +++ b/web_server/utils/path_manager.py @@ -1,20 +1,53 @@ +import os # Description: This file contains the PathManager class which is responsible for managing the paths of the stream data. class PathManager(): + def __init__(self) -> None: + self.root_path = "stream_data" + self.vods_path = os.path.join(self.root_path, "vods") + self.stream_path = os.path.join(self.root_path, "stream") + self.profile_pictures_path = os.path.join(self.root_path, "profile_pictures") + + self._create_root_directories() + + def _create_root_directories(self): + """ + Create directories for stream data if they do not exist + """ + if not os.path.exists(self.vods_path): + os.makedirs(self.vods_path) + + if not os.path.exists(self.stream_path): + os.makedirs(self.stream_path) + + if not os.path.exists(self.profile_pictures_path): + os.makedirs(self.profile_pictures_path) + + # Fix permissions + os.chmod(self.vods_path, 0o777) + os.chmod(self.stream_path, 0o777) + os.chmod(self.profile_pictures_path, 0o777) + def get_vods_path(self, username): - return f"stream_data/vods/{username}" + return os.path.join(self.vods_path, username) def get_stream_path(self, username): - return f"stream_data/stream/{username}" + return os.path.join(self.stream_path, username) def get_stream_file_path(self, username): - return f"{self.get_stream_path(username)}/index.m3u8" + return os.path.join(self.get_stream_path(username), "index.m3u8") def get_current_stream_thumbnail_file_path(self, username): - return f"{self.get_stream_path(username)}/index.jpg" + return os.path.join(self.get_stream_path(username), "index.jpg") def get_vod_file_path(self, username, vod_id): - return f"{self.get_vods_path(username)}/{vod_id}.mp4" + return os.path.join(self.get_vods_path(username), f"{vod_id}.mp4") def get_vod_thumbnail_file_path(self, username, vod_id): - return f"{self.get_vods_path(username)}/{vod_id}.jpg" \ No newline at end of file + return os.path.join(self.get_vods_path(username), f"{vod_id}.png") + + def get_profile_picture_file_path(self, username): + return os.path.join(self.profile_pictures_path, f"{username}.png") + + def get_profile_picture_path(self): + return self.profile_pictures_path \ No newline at end of file diff --git a/web_server/utils/stream_utils.py b/web_server/utils/stream_utils.py index b06b19b..1036290 100644 --- a/web_server/utils/stream_utils.py +++ b/web_server/utils/stream_utils.py @@ -143,7 +143,7 @@ def get_vod_tags(vod_id: int): """, (vod_id,)) return tags -def create_local_directories(username: str): +def create_user_directories(username: str): """ Create directories for user stream data if they do not exist """ From 9f13ccb1f27e6b5ca8c64c1ff4b263e6be0bfee6 Mon Sep 17 00:00:00 2001 From: ThisBirchWood Date: Fri, 28 Feb 2025 20:33:02 +0000 Subject: [PATCH 3/6] PATCH: Fixed incorrect path --- web_server/utils/path_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_server/utils/path_manager.py b/web_server/utils/path_manager.py index 2c849a9..fec3dbb 100644 --- a/web_server/utils/path_manager.py +++ b/web_server/utils/path_manager.py @@ -38,7 +38,7 @@ class PathManager(): return os.path.join(self.get_stream_path(username), "index.m3u8") def get_current_stream_thumbnail_file_path(self, username): - return os.path.join(self.get_stream_path(username), "index.jpg") + return os.path.join(self.get_stream_path(username), "index.png") def get_vod_file_path(self, username, vod_id): return os.path.join(self.get_vods_path(username), f"{vod_id}.mp4") From d204666d4807213a5875982adeeda9df1c363b82 Mon Sep 17 00:00:00 2001 From: ThisBirchWood Date: Fri, 28 Feb 2025 20:35:03 +0000 Subject: [PATCH 4/6] UPDATE: Reinstated thumbnail, vod routes in NGINX, and added a profile picture route --- nginx/nginx.conf | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 3e5ddf0..954bb80 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -93,22 +93,29 @@ http { expires -1d; } - #! Unused right now so the following are inaccurate locations - # The thumbnails location - # location ~ ^/stream/(.+)/thumbnails/(.+\.jpg)$ { - # alias /stream_data/$1/thumbnails/$2; + ## The thumbnails location + location ~ ^/stream/(.+)/thumbnails/(.+\.png)$ { + alias /stream_data/stream/$1/$2; - # # The thumbnails should not be cacheable - # expires -1d; - # } + # The thumbnails should not be cacheable + expires -1d; + } - # # The vods location - # location ~ ^/stream/(.+)/vods/(.+\.mp4)$ { - # alias /stream_data/$1/vods/$2; + ## The vods location + location ~ ^/stream/(.+)/vods/(.+\.mp4)$ { + alias /stream_data/vods/$1/$2; - # # The vods should not be cacheable - # expires -1d; - # } + # The vods should not be cacheable + expires -1d; + } + + ## Profile pictures location + location ~ ^/user/(.+)/profile_picture$ { + alias /stream_data/profile_pictures/$1.png; + + # The profile pictures should not be cacheable + expires -1d; + } # location ~ ^/\?token=.*$ { # proxy_pass http://frontend:5173; From 3dddcdd10766a5f753ecfcf3f4dbb366e49f2507 Mon Sep 17 00:00:00 2001 From: ThisBirchWood Date: Fri, 28 Feb 2025 20:35:58 +0000 Subject: [PATCH 5/6] UPDATE: Removed get profile picture route and moved it to NGINX, and updated how thumbnails were saved to use path_manager --- web_server/blueprints/streams.py | 2 +- web_server/blueprints/user.py | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 5e40ab8..4c8bc30 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -187,7 +187,7 @@ def init_stream(): # Create necessary directories username = user_info["username"] - create_local_directories(username) + create_user_directories(username) return redirect(f"/stream/{username}") diff --git a/web_server/blueprints/user.py b/web_server/blueprints/user.py index 0c6ee79..29066b2 100644 --- a/web_server/blueprints/user.py +++ b/web_server/blueprints/user.py @@ -4,6 +4,8 @@ from utils.auth import * from utils.utils import get_category_id from blueprints.middleware import login_required from utils.email import send_email, forgot_password_body, newsletter_conf +from utils.path_manager import PathManager +from celery_tasks.streaming import convert_image_to_png import redis from io import BytesIO @@ -14,6 +16,8 @@ r = redis.from_url(redis_url, decode_responses=True) user_bp = Blueprint("user", __name__) +path_manager = PathManager() + @user_bp.route('/user/') def user_data(username: str): """ @@ -42,13 +46,28 @@ def user_profile_picture_save(): """ Saves user profile picture """ - user_id = session.get("user_id") - image = request.files['image'] - ext = image.filename.split('.')[-1] + username = session.get("username") + thumbnail_path = path_manager.get_profile_picture_file_path(username) - image.save(f"/web_server/stream_data/{user_id}.{ext}") + # Check if the post request has the file part + if 'image' not in request.files: + return jsonify({"error": "No image found in request"}), 400 + + # Fetch image, convert to png, and save + image = Image.open(request.files['image']) + image.convert('RGB') + image.save(thumbnail_path, "PNG") - return "Success", 200 + return jsonify({"message": "Profile picture saved"}) + + +@user_bp.route('/user/profile_picture/') +def user_profile_picture(username: str): + """ + Returns the profile picture of a user + """ + user_id = get_user_id(username) + image = Image.open(f"/web_server/stream_data/{user_id}.jpg") @login_required @user_bp.route('/user/same/') From aa12f961a2df0e29a578611def1e03ad3a4c87ca Mon Sep 17 00:00:00 2001 From: JustIceO7 Date: Fri, 28 Feb 2025 23:42:11 +0000 Subject: [PATCH 6/6] CHANGE: Changed AuthModal responsiveness --- frontend/src/components/Auth/ForgotPasswordForm.tsx | 2 +- frontend/src/components/Auth/LoginForm.tsx | 2 +- frontend/src/components/Auth/OAuth.tsx | 2 +- frontend/src/components/Input/Button.tsx | 2 +- frontend/src/pages/UserPage.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Auth/ForgotPasswordForm.tsx b/frontend/src/components/Auth/ForgotPasswordForm.tsx index 4cae486..baf718c 100644 --- a/frontend/src/components/Auth/ForgotPasswordForm.tsx +++ b/frontend/src/components/Auth/ForgotPasswordForm.tsx @@ -71,7 +71,7 @@ const ForgotPasswordForm: React.FC = ({ onSubmit }) => { return (
-

+

Forgot Password

diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index cabad08..9170809 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -154,7 +154,7 @@ const LoginForm: React.FC = ({ onSubmit, onForgotPassword }) => { onClick={onForgotPassword} > - + Forgot Password diff --git a/frontend/src/components/Auth/OAuth.tsx b/frontend/src/components/Auth/OAuth.tsx index ed2c2e7..422ecda 100644 --- a/frontend/src/components/Auth/OAuth.tsx +++ b/frontend/src/components/Auth/OAuth.tsx @@ -20,7 +20,7 @@ export default function GoogleLogin() { alt="Google logo" className="w-[2em] h-[2em] mr-2" /> - + Sign in with Google diff --git a/frontend/src/components/Input/Button.tsx b/frontend/src/components/Input/Button.tsx index 6e64a2c..6245178 100644 --- a/frontend/src/components/Input/Button.tsx +++ b/frontend/src/components/Input/Button.tsx @@ -15,7 +15,7 @@ const Button: React.FC = ({ return (