From c7d443d8b655bf92515e9a13c8b9bf09b6832eb5 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Mon, 3 Mar 2025 01:47:47 +0000 Subject: [PATCH 1/4] FEAT: Add ability to download one's VODs from dashboard --- frontend/src/components/Layout/ListItem.tsx | 14 +-- frontend/src/components/Layout/ListRow.tsx | 1 - .../src/components/Navigation/Sidebar.tsx | 2 +- frontend/vite.config.dev.ts | 14 +-- nginx/nginx.conf | 4 +- web_server/utils/stream_utils.py | 88 ++++++++++++++++--- 6 files changed, 97 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/Layout/ListItem.tsx b/frontend/src/components/Layout/ListItem.tsx index 9967149..a39f3da 100644 --- a/frontend/src/components/Layout/ListItem.tsx +++ b/frontend/src/components/Layout/ListItem.tsx @@ -110,6 +110,7 @@ interface VodListItemProps extends BaseListItemProps, Omit { } const VodListItem: React.FC = ({ + vod_id, title, username, category_name, @@ -137,7 +138,7 @@ const VodListItem: React.FC = ({

{title}

-

{username}

+ {variant != "vodDashboard" &&

{username}

}

{category_name}

{datetime}

@@ -147,20 +148,21 @@ const VodListItem: React.FC = ({
{variant === "vodDashboard" && (
- - +
)}
diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index b44c54f..e52e087 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -164,7 +164,6 @@ const ListRow = forwardRef((props, ref) => { username={item.username} isLive={item.isLive} viewers={item.viewers} - thumbnail={item.thumbnail} onItemClick={() => onItemClick(item.username)} extraClasses={itemExtraClasses} /> diff --git a/frontend/src/components/Navigation/Sidebar.tsx b/frontend/src/components/Navigation/Sidebar.tsx index 1451a3d..6502847 100644 --- a/frontend/src/components/Navigation/Sidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar.tsx @@ -141,7 +141,7 @@ const Sidebar: React.FC = ({ extraClasses = "" }) => { return (
(window.location.href = `/category/${category.category_name}`)} > diff --git a/frontend/vite.config.dev.ts b/frontend/vite.config.dev.ts index 6904ffb..3c4e80d 100644 --- a/frontend/vite.config.dev.ts +++ b/frontend/vite.config.dev.ts @@ -20,11 +20,11 @@ export default defineConfig({ target: "http://localhost:8080", changeOrigin: true, ws: true, - }, - "/stream": { - target: "http://localhost:8080", - changeOrigin: true, - }, + }, + "/stream": { + target: "http://localhost:8080", + changeOrigin: true, + }, "/images": { target: "http://localhost:8080", changeOrigin: true, @@ -33,6 +33,10 @@ export default defineConfig({ target: "http://localhost:8080", changeOrigin: true, }, + "/vods": { + target: "http://localhost:8080", + changeOrigin: true, + }, }, }, build: { diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 3f2875c..a85dcb6 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -99,8 +99,8 @@ http { alias /user_data/$1/vods/$2; # where $1 is the user's username and $2 is the thumbnail_name - # The thumbnails should not be cacheable - expires -1d; + # The thumbnails should not be cacheable + expires -1d; } ## Profile pictures location diff --git a/web_server/utils/stream_utils.py b/web_server/utils/stream_utils.py index fd7263e..21165af 100644 --- a/web_server/utils/stream_utils.py +++ b/web_server/utils/stream_utils.py @@ -48,6 +48,80 @@ def get_current_stream_data(user_id: int) -> Optional[dict]: """, (user_id,)) return most_recent_stream +def end_user_stream(stream_key, user_id, username): + """ + Utility function to end a user's stream + + Parameters: + stream_key: The stream key of the user + user_id: The ID of the user + username: The username of the user + + Returns: + bool: True if stream was ended successfully, False otherwise + """ + from flask import current_app + from datetime import datetime + from dateutil import parser + from celery_tasks.streaming import combine_ts_stream + from utils.path_manager import PathManager + + path_manager = PathManager() + + if not stream_key or not user_id or not username: + current_app.logger.error("Cannot end stream - missing required information") + return False + + try: + # Open database connection + with Database() as db: + # Get stream info + stream_info = db.fetchone("""SELECT * + FROM streams + WHERE user_id = ?""", (user_id,)) + + # If user is not streaming, just return + if not stream_info: + current_app.logger.info(f"User {username} (ID: {user_id}) is not streaming") + return True, "User is not streaming" + + # Remove stream from database + db.execute("""DELETE FROM streams + WHERE user_id = ?""", (user_id,)) + + # Move stream to vod table + stream_length = int( + (datetime.now() - parser.parse(stream_info.get("start_time"))).total_seconds()) + + db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views) + VALUES (?, ?, ?, ?, ?, ?)""", (user_id, + stream_info.get("title"), + stream_info.get("start_time"), + stream_info.get("category_id"), + stream_length, + 0)) + + vod_id = db.get_last_insert_id() + + # Set user as not streaming + db.execute("""UPDATE users + SET is_live = 0 + WHERE user_id = ?""", (user_id,)) + + # Queue task to combine TS files into MP4 + combine_ts_stream.delay( + path_manager.get_stream_path(username), + path_manager.get_vods_path(username), + vod_id + ) + + current_app.logger.info(f"Stream ended for user {username} (ID: {user_id})") + return True, "Stream ended successfully" + + except Exception as e: + current_app.logger.error(f"Error ending stream for user {username}: {str(e)}") + return False, f"Error ending stream: {str(e)}" + def get_category_id(category_name: str) -> Optional[int]: """ Returns the category_id given a category name @@ -77,7 +151,7 @@ def get_vod(vod_id: int) -> dict: Returns data of a streamers vod """ with Database() as db: - vod = db.fetchone("""SELECT * FROM vods WHERE vod_id = ?;""", (vod_id,)) + vod = db.fetchone("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vod_id = ?;""", (vod_id,)) return vod def get_latest_vod(user_id: int): @@ -85,7 +159,7 @@ def get_latest_vod(user_id: int): Returns data of the most recent stream by a streamer """ with Database() as db: - latest_vod = db.fetchone("""SELECT vods.*, category_name FROM vods JOIN categories ON vods.category_id = categories.category_id WHERE user_id = ? ORDER BY vod_id DESC;""", (user_id,)) + latest_vod = db.fetchone("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vods.user_id = ? ORDER BY vod_id DESC;""", (user_id,)) return latest_vod def get_user_vods(user_id: int): @@ -93,15 +167,7 @@ def get_user_vods(user_id: int): Returns data of all vods by a streamer """ with Database() as db: - vods = db.fetchall("""SELECT vods.*, category_name FROM vods JOIN categories ON vods.category_id = categories.category_id WHERE user_id = ? ORDER BY vod_id DESC;""", (user_id,)) - return vods - -def get_all_vods(): - """ - Returns data of all VODs by all streamers in a JSON-compatible format - """ - with Database() as db: - vods = db.fetchall("""SELECT * FROM vods""") + vods = db.fetchall("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vods.user_id = ? ORDER BY vod_id DESC;""", (user_id,)) return vods def generate_thumbnail(stream_file: str, thumbnail_file: str) -> None: From 04d99928aa58195cce78d2c8b9df337161ec7c00 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Mon, 3 Mar 2025 01:55:01 +0000 Subject: [PATCH 2/4] FEAT: End any ongoing stream before a user logs out; UPDATE: Remove assignments to session after registering a new user; UPDATE: Create directories for user upon logging in in case they do not exist yet; --- web_server/blueprints/authentication.py | 30 ++++++++++++++++++------ web_server/utils/recommendation_utils.py | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/web_server/blueprints/authentication.py b/web_server/blueprints/authentication.py index 45f9313..3294732 100644 --- a/web_server/blueprints/authentication.py +++ b/web_server/blueprints/authentication.py @@ -95,12 +95,6 @@ def signup(): # Create user directories for stream data path_manager.create_user(username) - - # Create session for new user, to avoid them having unnecessary state info - session.clear() - session["username"] = username - session["user_id"] = get_user_id(username) - print(f"Logged in as {username}. session: {session.get('username')}. user_id: {session.get('user_id')}", flush=True) # send_email(username) return jsonify({ @@ -178,8 +172,11 @@ def login(): "error_fields": ["username", "password"], "message": "Invalid username or password" }), 401 + + # Add user directories for stream data in case they don't exist + path_manager.create_user(username) - # Set up session to avoid having unncessary state information + # Set up session session.clear() session["username"] = username session["user_id"] = get_user_id(username) @@ -209,8 +206,27 @@ def logout() -> dict: """ Log out and clear the users session. + If the user is currently streaming, end their stream first. Can only be accessed by a logged in user. """ + from database.database import Database + from utils.stream_utils import end_user_stream + + # Check if user is currently streaming + user_id = session.get("user_id") + username = session.get("username") + + with Database() as db: + is_streaming = db.fetchone("""SELECT is_live FROM users WHERE user_id = ?""", (user_id,)) + + if is_streaming and is_streaming.get("is_live") == 1: + # Get the user's stream key + stream_key_info = db.fetchone("""SELECT stream_key FROM users WHERE user_id = ?""", (user_id,)) + stream_key = stream_key_info.get("stream_key") if stream_key_info else None + + if stream_key: + # End the stream + end_user_stream(stream_key, user_id, username) session.clear() return {"logged_in": False} diff --git a/web_server/utils/recommendation_utils.py b/web_server/utils/recommendation_utils.py index aa393ff..a451940 100644 --- a/web_server/utils/recommendation_utils.py +++ b/web_server/utils/recommendation_utils.py @@ -94,7 +94,7 @@ def get_highest_view_categories(no_categories: int = 4, offset: int = 0) -> Opti """, (no_categories, offset)) return categories -def get_user_category_recommendations(user_id: 1, no_categories: int = 4) -> Optional[List[dict]]: +def get_user_category_recommendations(user_id = 1, no_categories: int = 4) -> Optional[List[dict]]: """ Queries user_preferences database to find users top favourite streaming category and returns the category """ From 45a0f364a088dcaf8af5655961fe8b2160871288 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Mon, 3 Mar 2025 11:05:10 +0000 Subject: [PATCH 3/4] UPDATE/REFACTOR: Rework of `UserPage`; FIX: Fallback for profile pictures; REFACTOR: Removal of unnecessary files & routes; REFACTOR: Improve documentation for flask routes; FIX: Correct data to return when fetching vods; --- frontend/config.tsx | 5 - frontend/src/hooks/useSameUser.ts | 24 ---- frontend/src/pages/UserPage.tsx | 225 ++++++++++++++---------------- frontend/src/types/UserType.ts | 1 - nginx/nginx.conf | 2 +- web_server/blueprints/streams.py | 116 ++++----------- web_server/blueprints/user.py | 11 -- web_server/utils/stream_utils.py | 9 +- 8 files changed, 138 insertions(+), 255 deletions(-) delete mode 100644 frontend/config.tsx delete mode 100644 frontend/src/hooks/useSameUser.ts diff --git a/frontend/config.tsx b/frontend/config.tsx deleted file mode 100644 index 59de80b..0000000 --- a/frontend/config.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export const paths = { - pfps: "", - category_thumbnails: "", - icons: "", -}; diff --git a/frontend/src/hooks/useSameUser.ts b/frontend/src/hooks/useSameUser.ts deleted file mode 100644 index 6bf874a..0000000 --- a/frontend/src/hooks/useSameUser.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useState } from "react" - -export function useSameUser({ username }: { username: string | undefined }) { - const [isSame, setIsSame] = useState(false); - - useEffect(() => { - const fetchStatus = async () => { - try { - const response = await fetch(`/api/user/same/${username}`); - if (!response.ok) { - throw new Error("Failed to validate user"); - } - const data = await response.json(); - setIsSame(data.same); - } catch (error) { - console.error("Error:", error); - } - }; - - fetchStatus(); - }, []); - - return isSame; -} \ No newline at end of file diff --git a/frontend/src/pages/UserPage.tsx b/frontend/src/pages/UserPage.tsx index ee3381c..fcffa20 100644 --- a/frontend/src/pages/UserPage.tsx +++ b/frontend/src/pages/UserPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import AuthModal from "../components/Auth/AuthModal"; import { useAuthModal } from "../hooks/useAuthModal"; import { useAuth } from "../context/AuthContext"; @@ -10,8 +10,8 @@ import DynamicPageContent from "../components/Layout/DynamicPageContent"; import LoadingScreen from "../components/Layout/LoadingScreen"; import { StreamListItem } from "../components/Layout/ListItem"; import { EditIcon } from "lucide-react"; -import { getCategoryThumbnail } from "../utils/thumbnailUtils"; -import { useSameUser } from "../hooks/useSameUser"; +import ListRow from "../components/Layout/ListRow"; +import { useStreams, useVods } from "../hooks/useContent"; interface UserProfileData { id: number; @@ -20,22 +20,38 @@ interface UserProfileData { followerCount: number; isPartnered: boolean; isLive: boolean; - currentStreamTitle?: string; - currentStreamCategory?: string; - currentStreamViewers?: number; - currentStreamStartTime?: string; - currentStreamThumbnail?: string; } const UserPage: React.FC = () => { - const [userPageVariant, setUserPageVariant] = useState<"personal" | "streamer" | "user" | "admin">("user"); + const [userPageVariant, setUserPageVariant] = useState<"personal" | "user" | "admin">("user"); const [profileData, setProfileData] = useState(); const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow(); const { showAuthModal, setShowAuthModal } = useAuthModal(); const { username: loggedInUsername } = useAuth(); const { username } = useParams(); - const isUser = useSameUser({ username }); + const { vods } = useVods(`/api/vods/${username}`); const navigate = useNavigate(); + const { streams } = useStreams(`/api/streams/${username}/data`); + const currentStream = streams[0]; + + const fetchProfileData = useCallback(async () => { + try { + // Profile data + const profileResponse = await fetch(`/api/user/${username}`); + const profileData = await profileResponse.json(); + setProfileData({ + id: profileData.user_id, + username: profileData.username, + bio: profileData.bio || "This user hasn't written a bio yet.", + followerCount: profileData.num_followers || 0, + isPartnered: profileData.isPartnered || false, + isLive: profileData.is_live, + }); + } catch (err) { + console.error("Error fetching profile data:", err); + window.location.href = "/404"; + } + }, [username]); // Saves uploaded image as profile picture for the user const saveUploadedImage = async (event: React.ChangeEvent) => { @@ -62,58 +78,20 @@ const UserPage: React.FC = () => { } }; + // Check if the current user is the currently logged-in user useEffect(() => { - // Fetch user profile data - fetch(`/api/user/${username}`) - .then((res) => res.json()) - .then((data) => { - setProfileData({ - id: data.user_id, - username: data.username, - bio: data.bio || "This user hasn't written a bio yet.", - followerCount: data.num_followers || 0, - isPartnered: data.isPartnered || false, - isLive: data.is_live, - currentStreamTitle: "", - currentStreamCategory: "", - currentStreamViewers: 0, - currentStreamThumbnail: "", - }); + if (username === loggedInUsername) setUserPageVariant("personal"); + // else if (data.isAdmin) setUserPageVariant("admin"); + else setUserPageVariant("user"); - if (data.is_live) { - // Fetch stream data for this streamer - fetch(`/api/streams/${data.user_id}/data`) - .then((res) => res.json()) - .then((streamData) => { - setProfileData((prevData) => { - if (!prevData) return prevData; - return { - ...prevData, - currentStreamTitle: streamData.title, - currentStreamCategory: streamData.category_id, - currentStreamViewers: streamData.num_viewers, - currentStreamStartTime: streamData.start_time, - currentStreamThumbnail: getCategoryThumbnail(streamData.category_name, streamData.thumbnail), - }; - }); - let variant: "user" | "streamer" | "personal" | "admin"; - if (username === loggedInUsername) variant = "personal"; - else if (streamData.title) variant = "streamer"; - // else if (data.isAdmin) variant = "admin"; - else variant = "user"; - setUserPageVariant(variant); - }) - .catch((err) => console.error("Error fetching stream data:", err)); - } - }) - .catch((err) => { - console.error("Error fetching profile data:", err); - navigate("/404"); - }); - - // Check if the *logged-in* user is following this user if (loggedInUsername && username) checkFollowStatus(username); - }, [username]); + }, [username, loggedInUsername, checkFollowStatus]); + + // Fetch user profile data + useEffect(() => { + if (!username) return; + fetchProfileData(); + }, [fetchProfileData]); if (!profileData) return ; @@ -148,16 +126,21 @@ const UserPage: React.FC = () => { } inset-0 z-20`} style={{ boxShadow: "var(--user-pfp-border-shadow)" }} > -
diff --git a/frontend/src/types/UserType.ts b/frontend/src/types/UserType.ts index 7dee37e..85a6722 100644 --- a/frontend/src/types/UserType.ts +++ b/frontend/src/types/UserType.ts @@ -6,5 +6,4 @@ export interface UserType { username: string; isLive: boolean; viewers: number; - thumbnail?: string; } \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf index a85dcb6..f3d97e7 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -104,7 +104,7 @@ http { } ## Profile pictures location - location ~ ^/user/(.+)/index.png$ { + location ~ ^/user/(.+)/profile_picture$ { alias /user_data/$1/index.png; # where $1 is the user's username diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 9c76328..b547229 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -67,13 +67,17 @@ def recommended_streams() -> list[dict]: return streams +@stream_bp.route('/streams//data') @stream_bp.route('/streams//data') -def stream_data(streamer_id) -> dict: +def stream_data(username=None, streamer_id=None) -> dict: """ Returns a streamer's current stream data """ + if username and not streamer_id: + streamer_id = get_user_id(username) data = get_current_stream_data(streamer_id) + # If the user is the streamer, return the stream key also if session.get('user_id') == streamer_id: with Database() as db: stream_key = db.fetchone( @@ -112,33 +116,21 @@ def recommended_categories() -> list | list[dict]: """ user_id = session.get("user_id") - categories = get_user_category_recommendations(1) + categories = get_user_category_recommendations(user_id) return jsonify(categories) @login_required -@stream_bp.route('/categories/following') +@stream_bp.route('/streams/followed_categories') def following_categories_streams(): """ - Returns popular streams in categories which the user followed + Returns popular streams from categories the user is following """ streams = get_followed_categories_recommendations(session.get('user_id')) return jsonify(streams) -@login_required -@stream_bp.route('/categories/your_categories') -def following_your_categories(): - """ - Returns categories which the user followed - """ - - streams = get_followed_your_categories(session.get('user_id')) - return jsonify(streams) - - - # User Routes @stream_bp.route('/user//status') def user_live_status(username): @@ -172,7 +164,7 @@ def user_live_status(username): @stream_bp.route('/vods/') def vod(vod_id): """ - Returns a JSON of a vod + Returns details about a specific vod """ vod = get_vod(vod_id) return jsonify(vod) @@ -187,6 +179,7 @@ def vods(username): "vod_id": int, "title": str, "datetime": str, + "username": str, "category_name": str, "length": int (in seconds), "views": int @@ -204,10 +197,8 @@ def get_all_vods(): Returns data of all VODs by all streamers in a JSON-compatible format """ with Database() as db: - vods = db.fetchall("SELECT * FROM vods") - - print("Fetched VODs from DB:", vods) - + vods = db.fetchall("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id;""") + return jsonify(vods) # RTMP Server Routes @@ -355,23 +346,12 @@ def update_stream(): @stream_bp.route("/end_stream", methods=["POST"]) def end_stream(): """ - Ends a stream - - step-by-step: - remove stream from database - move stream to vod table - set user as not streaming - convert ts files to mp4 - clean up old ts files - end thumbnail generation + Ends a stream based on the HTTP request """ + print("Ending stream", flush=True) - print("TEST END STREAM") stream_key = request.get_json().get("key") - print(stream_key, flush=True) - user_id = None - username = None - + if not stream_key: # Try getting stream_key from form data (for nginx in the case that the stream is ended on OBS's end) stream_key = request.form.get("name") @@ -380,60 +360,24 @@ def end_stream(): print("Unauthorized - No stream key provided", flush=True) return "Unauthorized", 403 - # Open database connection + # Get user info from stream key with Database() as db: - initial_streams = db.fetchall("""SELECT title FROM streams""") - print("Initial streams:", initial_streams, flush=True) - - # Get user info from stream key - user_info = db.fetchone("""SELECT * + user_info = db.fetchone("""SELECT user_id, username FROM users WHERE stream_key = ?""", (stream_key,)) - - stream_info = db.fetchone("""SELECT * - FROM streams - WHERE user_id = ?""", (user_id,)) - - print("Got stream_info", stream_info, flush=True) - - # If stream key is invalid, return unauthorized + if not user_info: print("Unauthorized - No user found from stream key", flush=True) return "Unauthorized", 403 - # If stream never published, return - if not stream_info: - print(f"Stream for stream key: {stream_key} never began", flush=True) - return "Stream ended", 200 - - # Remove stream from database - db.execute("""DELETE FROM streams - WHERE user_id = ?""", (user_id,)) - - # Move stream to vod table - stream_length = int( - (datetime.now() - parser.parse(stream_info.get("start_time"))).total_seconds()) - - db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views) - VALUES (?, ?, ?, ?, ?, ?)""", (user_id, - stream_info.get("title"), - stream_info.get( - "start_time"), - stream_info.get( - "category_id"), - stream_length, - 0)) - - vod_id = db.get_last_insert_id() - - # Set user as not streaming - db.execute("""UPDATE users - SET is_live = 0 - WHERE user_id = ?""", (user_id,)) - - current_streams = db.fetchall("""SELECT title FROM streams""") - - combine_ts_stream.delay(path_manager.get_stream_path( - username), path_manager.get_vods_path(username), vod_id) - - print("Stream ended. Current streams now:", current_streams, flush=True) - return "Stream ended", 200 + + user_id = user_info["user_id"] + username = user_info["username"] + + result, message = end_user_stream(stream_key, user_id, username) + + if result: + print(f"Stream ended: {message}", flush=True) + return "Stream ended", 200 + else: + print(f"Error ending stream: {message}", flush=True) + return "Error ending stream", 500 diff --git a/web_server/blueprints/user.py b/web_server/blueprints/user.py index 98e855f..3f394d7 100644 --- a/web_server/blueprints/user.py +++ b/web_server/blueprints/user.py @@ -61,17 +61,6 @@ def user_profile_picture_save(): return jsonify({"message": "Profile picture saved", "path": thumbnail_path}) -@login_required -@user_bp.route('/user/same/') -def user_is_same(username): - """ - Returns if given user is current user - """ - current_username = session.get("username") - if username == current_username: - return jsonify({"same": True}) - return jsonify({"same": False}) - ## Subscription Routes @login_required @user_bp.route('/user/subscription/') diff --git a/web_server/utils/stream_utils.py b/web_server/utils/stream_utils.py index 21165af..119f745 100644 --- a/web_server/utils/stream_utils.py +++ b/web_server/utils/stream_utils.py @@ -67,9 +67,10 @@ def end_user_stream(stream_key, user_id, username): from utils.path_manager import PathManager path_manager = PathManager() + print(f"Ending stream for user {username} (ID: {user_id})", flush=True) if not stream_key or not user_id or not username: - current_app.logger.error("Cannot end stream - missing required information") + print("Cannot end stream - missing required information", flush=True) return False try: @@ -82,7 +83,7 @@ def end_user_stream(stream_key, user_id, username): # If user is not streaming, just return if not stream_info: - current_app.logger.info(f"User {username} (ID: {user_id}) is not streaming") + print(f"User {username} (ID: {user_id}) is not streaming", flush=True) return True, "User is not streaming" # Remove stream from database @@ -115,11 +116,11 @@ def end_user_stream(stream_key, user_id, username): vod_id ) - current_app.logger.info(f"Stream ended for user {username} (ID: {user_id})") + print(f"Stream ended for user {username} (ID: {user_id})", flush=True) return True, "Stream ended successfully" except Exception as e: - current_app.logger.error(f"Error ending stream for user {username}: {str(e)}") + print(f"Error ending stream for user {username}: {str(e)}", flush=True) return False, f"Error ending stream: {str(e)}" def get_category_id(category_name: str) -> Optional[int]: From bc8f93564822a2aeada293a1b0c6266c56687735 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Mon, 3 Mar 2025 11:09:24 +0000 Subject: [PATCH 4/4] FIX: `end_stream` route input data handling; PATCH/UPDATE: Handle cases where content fetched is falsy/empty --- frontend/package.json | 2 +- frontend/src/components/Stream/StreamDashboard.tsx | 12 +++++++----- frontend/src/hooks/useContent.ts | 8 +++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 2f9b28a..5952062 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.5.0", + "version": "0.15.0", "type": "module", "scripts": { "dev": "vite --config vite.config.dev.ts", diff --git a/frontend/src/components/Stream/StreamDashboard.tsx b/frontend/src/components/Stream/StreamDashboard.tsx index 0c9aaa0..c33ebac 100644 --- a/frontend/src/components/Stream/StreamDashboard.tsx +++ b/frontend/src/components/Stream/StreamDashboard.tsx @@ -225,13 +225,13 @@ const StreamDashboard: React.FC = ({ username, userId, isL const handleEndStream = async () => { console.log("Ending stream..."); - const formData = new FormData(); - formData.append("key", streamData.stream_key); - try { const response = await fetch("/api/end_stream", { method: "POST", - body: formData, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ key: streamData.stream_key }), }); if (response.ok) { @@ -375,7 +375,9 @@ const StreamDashboard: React.FC = ({ username, userId, isL streamCategory={streamData.category_name || "Category"} viewers={streamData.viewer_count} thumbnail={thumbnailPreview.url || ""} - onItemClick={() => {}} + onItemClick={() => { + window.open(`/${username}`, "_blank"); + }} extraClasses="max-w-[20vw]" /> diff --git a/frontend/src/hooks/useContent.ts b/frontend/src/hooks/useContent.ts index 64084d3..14c6357 100644 --- a/frontend/src/hooks/useContent.ts +++ b/frontend/src/hooks/useContent.ts @@ -23,6 +23,7 @@ const processVodData = (data: any[]): VodType[] => { // Helper function to process API data into our consistent types const processStreamData = (data: any[]): StreamType[] => { + if (!data || data.length === 0 || !data[0] || !data[0].user_id) return []; return data.map((stream) => ({ type: "stream", id: stream.user_id, @@ -76,8 +77,9 @@ export function useFetchContent( throw new Error(`Error fetching data: ${response.status}`); } - const rawData = await response.json(); - const processedData = processor(rawData); + const rawData = await response.json(); + let processedData = processor(Array.isArray(rawData) ? rawData : (rawData ? [rawData] : [])); + console.log("processedData", processedData); setData(processedData); setError(null); } catch (err) { @@ -126,7 +128,7 @@ export function useVods(customUrl?: string): { isLoading: boolean; error: string | null; } { - const url = customUrl || "api/vods/all"; //TODO: Change this to the correct URL or implement it + const url = customUrl || "api/vods/all"; const { data, isLoading, error } = useFetchContent(url, processVodData, [customUrl]); return { vods: data, isLoading, error };