From 6586506c9702cab87666f975dc55051bb2dbe239 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Thu, 30 Jan 2025 03:42:22 +0000 Subject: [PATCH] MAJOR Fix: Resolved API Request Delays; Feat: Added a dev test account to users for expedited login; Refactor: Improve socket connection handling and add logging for API request duration & update to ListRow key generation for improved uniqueness; Feat: Made it so streams with no set thumbnail use their category's thumbnail; Minor Fix: Corrections to db recommendation methods; --- .../thumbnails/categories/education.webp | Bin 0 -> 31262 bytes frontend/src/components/Layout/ListRow.tsx | 2 +- frontend/src/components/Video/ChatPanel.tsx | 13 ++- frontend/src/context/SocketContext.tsx | 103 +++++++++++++----- frontend/src/context/StreamsContext.tsx | 27 +++-- frontend/src/main.tsx | 4 +- frontend/src/pages/VideoPage.tsx | 32 +++--- nginx/nginx.conf | 30 ++--- web_server/Dockerfile | 2 +- web_server/blueprints/__init__.py | 11 +- web_server/blueprints/chat.py | 5 +- web_server/blueprints/socket.py | 3 + web_server/blueprints/streams.py | 2 +- web_server/blueprints/utils.py | 10 ++ web_server/database/app.db | Bin 73728 -> 73728 bytes web_server/database/testing_data.sql | 18 +-- web_server/database/users.sql | 3 - web_server/requirements.txt | 4 +- web_server/utils/recommendation_utils.py | 46 ++++---- 19 files changed, 197 insertions(+), 118 deletions(-) create mode 100644 frontend/public/images/thumbnails/categories/education.webp create mode 100644 web_server/blueprints/socket.py diff --git a/frontend/public/images/thumbnails/categories/education.webp b/frontend/public/images/thumbnails/categories/education.webp new file mode 100644 index 0000000000000000000000000000000000000000..aad0bccb5edadf77ba1fc0be16fe52c54448129c GIT binary patch literal 31262 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index 3649fb2..4e15c95 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -68,7 +68,7 @@ const ListRow: React.FC = ({
{items.map((item) => ( = ({ streamId }) => { // Join chat room when component mounts useEffect(() => { if (socket && isConnected) { + // Join chat room socket.emit("join", { stream_id: streamId }); + // Handle beforeunload event + const handleBeforeUnload = () => { + socket.emit("leave", { stream_id: streamId }); + socket.disconnect(); + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + // Load initial chat history fetch(`/api/chat/${streamId}`) .then((response) => { @@ -45,9 +54,11 @@ const ChatPanel: React.FC = ({ streamId }) => { setMessages((prev) => [...prev, data]); }); - // Cleanup + // Cleanup function return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); socket.emit("leave", { stream_id: streamId }); + socket.disconnect(); socket.off("new_message"); }; } diff --git a/frontend/src/context/SocketContext.tsx b/frontend/src/context/SocketContext.tsx index b75db69..203e401 100644 --- a/frontend/src/context/SocketContext.tsx +++ b/frontend/src/context/SocketContext.tsx @@ -1,5 +1,5 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { io, Socket } from 'socket.io-client'; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; +import { io, Socket } from "socket.io-client"; interface SocketContextType { socket: Socket | null; @@ -8,39 +8,92 @@ interface SocketContextType { const SocketContext = createContext(undefined); -export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const socketRef = useRef(null); useEffect(() => { - const newSocket = io("http://localhost:8080", { - path: "/socket.io/", + console.log("Start of useEffect"); + + // Check if we already have a socket instance + if (socketRef.current) { + console.log("Socket already exists, closing existing socket"); + socketRef.current.close(); + } + + console.log("Creating new socket connection"); + const newSocket = io('http://localhost:8080', { + path: '/socket.io/', + transports: ['websocket', 'polling'], withCredentials: true, - transports: ['websocket'], - upgrade: false + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + reconnectionAttempts: 5, + timeout: 5000 }); - - newSocket.on('connect', () => { - console.log('Socket connected!'); - setIsConnected(true); - }); - - newSocket.on('connect_error', (error) => { - console.error('Socket connection error:', error); - }); - - newSocket.on('disconnect', () => { - console.log('Socket disconnected!'); - setIsConnected(false); - }); - + + socketRef.current = newSocket; setSocket(newSocket); + + newSocket.on("connect", () => { + console.log("Socket connected!"); + setIsConnected(true); + setIsLoading(false); + }); + + newSocket.on("reconnect_attempt", (attemptNumber) => { + console.log(`Reconnecting... Attempt ${attemptNumber}`); + }); + + newSocket.on("reconnect_error", (error) => { + console.error("Reconnection error:", error); + }); + + newSocket.on("reconnect", (attemptNumber) => { + console.log(`Reconnected after ${attemptNumber} attempts!`); + }); + + newSocket.on("reconnect_failed", () => { + console.error("Reconnection failed. Please refresh the page."); + }); + + newSocket.on("connect_error", (error) => { + console.error("Socket connection error:", error); + setIsLoading(false); + if (newSocket) newSocket.disconnect(); + newSocket.connect(); + }); + + newSocket.on("disconnect", (reason) => { + console.log( + "Socket disconnected! Reason: " + reason + " - Attempting reconnect..." + ); + setIsConnected(false); + newSocket.connect(); + }); return () => { - newSocket.close(); + if (socketRef.current) { + console.log("Cleaning up socket connection..."); + socketRef.current.disconnect(); + socketRef.current.close(); + socketRef.current = null; + } }; }, []); + if (isLoading) { + return ( +
+
Connecting to socket...
+
+ ); + } + return ( {children} @@ -51,7 +104,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr export const useSocket = () => { const context = useContext(SocketContext); if (context === undefined) { - throw new Error('useSocket must be used within a SocketProvider'); + throw new Error("useSocket must be used within a SocketProvider"); } return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/context/StreamsContext.tsx b/frontend/src/context/StreamsContext.tsx index 5a1d69e..a211992 100644 --- a/frontend/src/context/StreamsContext.tsx +++ b/frontend/src/context/StreamsContext.tsx @@ -48,8 +48,15 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) { title: stream.title, streamer: stream.username, viewers: stream.num_viewers, - thumbnail: stream.thumbnail, + thumbnail: + stream.thumbnail || + `/images/thumbnails/categories/${stream.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + category: stream.category_name, })); + + console.log(extractedData); setFeaturedStreams(extractedData); }); @@ -57,15 +64,15 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) { fetch(fetch_url[1]) .then((response) => response.json()) .then((data: CategoryItem[]) => { - const extractedData: CategoryItem[] = data.map( - (category: any) => ({ - type: "category", - id: category.category_id, - title: category.category_name, - viewers: category.num_viewers, - thumbnail: `/images/thumbnails/categories/${category.category_name.toLowerCase().replace(/ /g, "_")}.webp` - }) - ); + const extractedData: CategoryItem[] = data.map((category: any) => ({ + type: "category", + id: category.category_id, + title: category.category_name, + viewers: category.num_viewers, + thumbnail: `/images/thumbnails/categories/${category.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + })); console.log(extractedData); setFeaturedCategories(extractedData); }); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d0db8b3..9bc1251 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,7 +4,7 @@ import './assets/styles/index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - + // - , + // , ) diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index c4ccbef..533a97d 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -23,24 +23,24 @@ interface StreamDataProps { const VideoPage: React.FC = ({ streamId }) => { const { isLoggedIn } = useAuth(); - const [showCheckout, setShowCheckout] = useState(false); + // const [showCheckout, setShowCheckout] = useState(false); const showReturn = window.location.search.includes("session_id"); const { streamerName } = useParams<{ streamerName: string }>(); const [streamData, setStreamData] = useState(); const navigate = useNavigate(); - useEffect(() => { - // Prevent scrolling when checkout is open - if (showCheckout) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = "unset"; - } - // Cleanup function to ensure overflow is restored when component unmounts - return () => { - document.body.style.overflow = "unset"; - }; - }, [showCheckout]); + // useEffect(() => { + // // Prevent scrolling when checkout is open + // if (showCheckout) { + // document.body.style.overflow = "hidden"; + // } else { + // document.body.style.overflow = "unset"; + // } + // // Cleanup function to ensure overflow is restored when component unmounts + // return () => { + // document.body.style.overflow = "unset"; + // }; + // }, [showCheckout]); useEffect(() => { // Fetch stream data for this streamer fetch( @@ -83,7 +83,7 @@ const VideoPage: React.FC = ({ streamId }) => { > {isLoggedIn && (
- {showCheckout && setShowCheckout(false)} />} - {showReturn && } + {/* {showCheckout && setShowCheckout(false)} />} */} + {/* {showReturn && } */} ); }; diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 199c658..13d7b88 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -43,6 +43,23 @@ http { listen 8080; root /var/www; + location /api/ { + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://web_server:5000; # flask-app is the name of the Flask container in docker-compose + } + + location /socket.io/ { + proxy_pass http://web_server:5000/socket.io/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } + + # The MPEG-TS video chunks are stored in /tmp/hls location ~ ^/stream/user/(.+\.ts)$ { alias /tmp/hls/$1; @@ -59,19 +76,6 @@ http { expires -1d; } - location /api/ { - rewrite ^/api/(.*)$ /$1 break; - proxy_pass http://web_server:5000; # flask-app is the name of the Flask container in docker-compose - } - - location /socket.io/ { - proxy_pass http://web_server:5000/socket.io/; - proxy_http_version 1.1; - proxy_buffering off; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - } - location / { proxy_pass http://frontend:5173; # frontend is the name of the React container in docker-compose } diff --git a/web_server/Dockerfile b/web_server/Dockerfile index e4d1d2d..93bbc35 100644 --- a/web_server/Dockerfile +++ b/web_server/Dockerfile @@ -16,4 +16,4 @@ COPY . . ENV FLASK_APP=blueprints.__init__ ENV FLASK_DEBUG=True -CMD ["gunicorn", "-b", "0.0.0.0:5000", "blueprints.__init__:create_app()"] +CMD ["python", "-c", "from blueprints.socket import socketio; from blueprints.__init__ import create_app; app = create_app(); socketio.run(app, host='0.0.0.0', port=5000, debug=True)"] \ No newline at end of file diff --git a/web_server/blueprints/__init__.py b/web_server/blueprints/__init__.py index a494d87..6b36f7a 100644 --- a/web_server/blueprints/__init__.py +++ b/web_server/blueprints/__init__.py @@ -1,7 +1,7 @@ from flask import Flask from flask_session import Session from flask_cors import CORS -from blueprints.utils import logged_in_user +from blueprints.utils import logged_in_user, record_time from blueprints.errorhandlers import register_error_handlers # from flask_wtf.csrf import CSRFProtect, generate_csrf @@ -9,7 +9,8 @@ from blueprints.authentication import auth_bp from blueprints.stripe import stripe_bp from blueprints.user import user_bp from blueprints.streams import stream_bp -from blueprints.chat import chat_bp, socketio +from blueprints.chat import chat_bp +from blueprints.socket import socketio from os import getenv @@ -29,8 +30,11 @@ def create_app(): CORS(app, supports_credentials=True) # csrf.init_app(app) + socketio.init_app(app) + Session(app) app.before_request(logged_in_user) + app.after_request(record_time) # adds in error handlers register_error_handlers(app) @@ -48,7 +52,6 @@ def create_app(): app.register_blueprint(stream_bp) app.register_blueprint(chat_bp) - # Tell sockets where the initialisation app is - socketio.init_app(app, cors_allowed_origins="*") + socketio.init_app(app) return app diff --git a/web_server/blueprints/chat.py b/web_server/blueprints/chat.py index 0ace4a9..2222731 100644 --- a/web_server/blueprints/chat.py +++ b/web_server/blueprints/chat.py @@ -1,11 +1,10 @@ from flask import Blueprint, jsonify, session from database.database import Database -from flask_socketio import SocketIO, emit, join_room, leave_room +from .socket import socketio +from flask_socketio import emit, join_room, leave_room from datetime import datetime -from flask_socketio import SocketIO chat_bp = Blueprint("chat", __name__) -socketio = SocketIO() # <---------------------- ROUTES NEEDS TO BE CHANGED TO VIDEO OR DELETED AS DEEMED APPROPRIATE ----------------------> diff --git a/web_server/blueprints/socket.py b/web_server/blueprints/socket.py new file mode 100644 index 0000000..41a9794 --- /dev/null +++ b/web_server/blueprints/socket.py @@ -0,0 +1,3 @@ +from flask_socketio import SocketIO + +socketio = SocketIO(cors_allowed_origins="*", async_mode='gevent', logger=True, engineio_logger=True) diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index e81082b..1013778 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -120,7 +120,7 @@ def get_following_categories_streams(): """ Returns popular streams in categories which the user followed """ - streams = followed_categories_recommendations() + streams = followed_categories_recommendations(get_user_id(session.get('username'))) return jsonify(streams) diff --git a/web_server/blueprints/utils.py b/web_server/blueprints/utils.py index b808fdb..1528ffb 100644 --- a/web_server/blueprints/utils.py +++ b/web_server/blueprints/utils.py @@ -1,14 +1,24 @@ from flask import redirect, url_for, request, g, session from functools import wraps from re import match +from time import time def logged_in_user(): """ Validator to make sure a user is logged in. """ + g.start_time = time() g.user = session.get("username", None) g.admin = session.get("username", None) +def record_time(response): + if hasattr(g, 'start_time'): + time_taken = time() - g.start_time + print(f"Request to {request.endpoint} took {time_taken:.4f} seconds", flush=True) + else: + print("No start time found", flush=True) + return response + def login_required(view): """ Add at start of routes where users need to be logged in to access. diff --git a/web_server/database/app.db b/web_server/database/app.db index c7a7cf1e8de2fcf01ec1070ec501d63a03444da7..b83ae031cc7ac8cbd59f92bfc7ac56eb26744e53 100644 GIT binary patch delta 672 zcmY+BO=}ZT6o&6)CYdIaWRlc0wKh5_0TG%qb7zt}6NOZ)xF}Xs{8|X!%tsq-nzTt% zbkpXR?i6fq=?^F_O2L91@MD*SJ4M{siu4!QjUc)xqE10U;XI4?960BJv(VudI^5&F zgs&F>knjk5_yAzGmIvXW*B!e1KEi$mBG?7uR&byz8-B^}@Ml7M{x$Zi|F#dZ8_^Qj z4Xx5l>>@{Qkbe3qaQfN1G!vR3NBLAP$1J7jH|7QH=LGuPm!_>yf$m4+bRcxjA39Yc zeY7N&BK}7p=$!_DI3(p_)?wHj>7{Kk7YVNY87&Tw9`_Bm&T#D>a)fS&*4RiK9t%f;szuqjW5Eb;IUR1KUHDhRI-mFj78e`W+%&Y^;SVpQY z>#~DQY~j3u728sgTz2$=p%{j(K*i2$$gv9=MzW#Vj;yM3Id7FAE+fZrbcAGCH|BK& z4Fe%ZGcBk&3NjT`Hf-ILO+&R2)D+9K90TJ#gjU{CkO~X1;8_ljS8-)>$gEWlHO@*+ zyU~=eY1U?^no_eSHE_$8niJRywpFRkHl#*nd}_8X+1Q+rnw6@ZxfKwAyOF*C8Q%SJ xY59bp%90F=idTp}tmXd8uWQP`dAlwS$Ku|vVjhP-<2~HC{5?wL*#3Iw&Tn)dyXgP` delta 421 zcmZoTz|wGlWrCCt;~5491|cBk0b(Ww2F9w1I!2~IQ9YdiUbb8YeujPq{&g(JnPd6u z7+>+*@SbPrXW!56&2y34gY7TZI*v;~eGCju;f+NbZ$~h4rBST0 zVzLAKH11~B#xizsQBlU$+{uS|f|wN)bS4XOi*MHD6#}ZNYAl_s!SRc`DYLPVU0hL- zu~l}m7vG)9D;V7;=kwb$+cy?WUd?RJtlOA3Ie}Go@;d%70XE(x4E&S$b@W|jBJ}f_)nDQHnLPOG_x|Yure{8+>@`1!atI4h|J$q!0#Xc^g07O U5X0!rf(GCCCL0L6UUVP<08%M+UjP6A diff --git a/web_server/database/testing_data.sql b/web_server/database/testing_data.sql index 5873c31..4fcf8cf 100644 --- a/web_server/database/testing_data.sql +++ b/web_server/database/testing_data.sql @@ -47,7 +47,8 @@ INSERT INTO subscribes (user_id, subscribed_id, since, expires) VALUES (5, 105, '2024-08-30', '2025-02-28'); INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio) VALUES -('GamerDude2', 'password123', 'gamerdude3@gmail.com', 3200, '7890', 0, 'Streaming my gaming adventures!'); +('GamerDude2', 'password123', 'gamerdude3@gmail.com', 3200, '7890', 0, 'Streaming my gaming adventures!'), +('dev', 'scrypt:32768:8:1$avr94c5cplosNUDc$f2ba0738080facada51a1ed370bf869199e121e547fe64a7094ef0330b5db2ab7fff87700898729977f4cd24f17c17b9e8c0c93e7241dcdf9aa522d5d1732626', 'dev@gmail.com', 1, '8080', 0, 'A test account to save that tedious signup each time!'); INSERT INTO chat (stream_id, chatter_id, message) VALUES (1, 'Susan', 'Hey Every, loving the stream'), @@ -67,17 +68,4 @@ SELECT * FROM stream_tags; -- To see all tables in the database SELECT name FROM sqlite_master WHERE type='table'; - -SELECT isLive FROM streams WHERE user_id = '5'; - - - -SELECT * -FROM ( - SELECT chatter_id, message, time_sent - FROM chat - WHERE stream_id = 1 - ORDER BY time_sent DESC - LIMIT 50 -) -ORDER BY time_sent ASC \ No newline at end of file +INSERT INTO users \ No newline at end of file diff --git a/web_server/database/users.sql b/web_server/database/users.sql index c395bb0..331401e 100644 --- a/web_server/database/users.sql +++ b/web_server/database/users.sql @@ -1,6 +1,3 @@ --- View all tables in the database -SELECT name FROM sqlite_master WHERE type='table'; - DROP TABLE IF EXISTS users; CREATE TABLE users ( diff --git a/web_server/requirements.txt b/web_server/requirements.txt index 0c7d06e..14a4a9e 100644 --- a/web_server/requirements.txt +++ b/web_server/requirements.txt @@ -21,4 +21,6 @@ typing_extensions==4.12.2 urllib3==2.3.0 Werkzeug==3.1.3 WTForms==3.2.1 -Gunicorn==20.1.0 \ No newline at end of file +Gunicorn==20.1.0 +gevent>=22.10.2 +gevent-websocket \ No newline at end of file diff --git a/web_server/utils/recommendation_utils.py b/web_server/utils/recommendation_utils.py index 092dbc7..b934537 100644 --- a/web_server/utils/recommendation_utils.py +++ b/web_server/utils/recommendation_utils.py @@ -15,20 +15,22 @@ def user_recommendation_category(user_id: int) -> Optional[int]: """, (user_id,)) return data -def followed_categories_recommendations(user_id: int): +#TODO Needs to be reworked to get categories instead of streams of categories (below can be done in another function - get_streams_by_category) +def followed_categories_recommendations(user_id : int): """ Returns top 25 streams given a users category following """ with Database() as db: categories = db.fetchall(""" - SELECT users.user_id, title, username, num_viewers, category_name - FROM streams - WHERE category_id IN (SELECT category_id FROM categories WHERE user_id = ?) - ORDER BY num_viewers DESC - LIMIT 25; - """, (user_id,)) + SELECT user_id, title, num_viewers, categories.category_name + FROM streams + JOIN categories ON streams.category_id = categories.category_id + WHERE category_id IN (SELECT category_id FROM categories WHERE user_id = ?) + ORDER BY num_viewers DESC + LIMIT 25; """, (user_id,)) return categories +#TODO Needs to be reworked to get categories instead of streams of categories def recommendations_based_on_category(category_id: int) -> Optional[List[Tuple[int, str, int]]]: """ Queries stream database to get top 25 most viewed streams based on given category and returns @@ -36,14 +38,14 @@ def recommendations_based_on_category(category_id: int) -> Optional[List[Tuple[i """ with Database() as db: data = db.fetchall(""" - SELECT users.user_id, title, username, num_viewers, category_name - FROM streams - JOIN users ON users.user_id = streams.user_id - JOIN categories ON streams.category_id = categories.category_id - WHERE categories.category_id = ? - ORDER BY num_viewers DESC - LIMIT 25 - """, (category_id,)) + SELECT streams.category_id, streams.user_id, streams.title, users.username, streams.num_viewers, categories.category_name + FROM streams + JOIN users ON users.user_id = streams.user_id + JOIN categories ON streams.category_id = categories.category_id + WHERE categories.category_id = ? + ORDER BY num_viewers DESC + LIMIT 25 + """, (category_id,)) return data def default_recommendations(): @@ -53,13 +55,13 @@ def default_recommendations(): """ with Database() as db: data = db.fetchall(""" - SELECT users.user_id, title, username, num_viewers, category_name - FROM streams - JOIN users ON users.user_id = streams.user_id - JOIN categories ON streams.category_id = categories.category_id - ORDER BY num_viewers DESC - LIMIT 25; - """) + SELECT stream_id, users.user_id, title, username, num_viewers, category_name + FROM streams + JOIN users ON users.user_id = streams.user_id + JOIN categories ON streams.category_id = categories.category_id + ORDER BY num_viewers DESC + LIMIT 25; + """) return data def category_recommendations():