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;
This commit is contained in:
Chris-1010
2025-01-30 03:42:22 +00:00
parent 6cfac0d78f
commit 6586506c97
19 changed files with 197 additions and 118 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -68,7 +68,7 @@ const ListRow: React.FC<ListRowProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{items.map((item) => ( {items.map((item) => (
<ListItem <ListItem
key={item.id} key={`${item.type}-${item.id}`}
id={item.id} id={item.id}
type={item.type} type={item.type}
title={item.title} title={item.title}

View File

@@ -23,8 +23,17 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId }) => {
// Join chat room when component mounts // Join chat room when component mounts
useEffect(() => { useEffect(() => {
if (socket && isConnected) { if (socket && isConnected) {
// Join chat room
socket.emit("join", { stream_id: streamId }); 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 // Load initial chat history
fetch(`/api/chat/${streamId}`) fetch(`/api/chat/${streamId}`)
.then((response) => { .then((response) => {
@@ -45,9 +54,11 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId }) => {
setMessages((prev) => [...prev, data]); setMessages((prev) => [...prev, data]);
}); });
// Cleanup // Cleanup function
return () => { return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
socket.emit("leave", { stream_id: streamId }); socket.emit("leave", { stream_id: streamId });
socket.disconnect();
socket.off("new_message"); socket.off("new_message");
}; };
} }

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useRef, useState } from "react";
import { io, Socket } from 'socket.io-client'; import { io, Socket } from "socket.io-client";
interface SocketContextType { interface SocketContextType {
socket: Socket | null; socket: Socket | null;
@@ -8,39 +8,92 @@ interface SocketContextType {
const SocketContext = createContext<SocketContextType | undefined>(undefined); const SocketContext = createContext<SocketContextType | undefined>(undefined);
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const socketRef = useRef<Socket | null>(null);
useEffect(() => { useEffect(() => {
const newSocket = io("http://localhost:8080", { console.log("Start of useEffect");
path: "/socket.io/",
// 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, withCredentials: true,
transports: ['websocket'], reconnectionDelay: 1000,
upgrade: false reconnectionDelayMax: 5000,
reconnectionAttempts: 5,
timeout: 5000
}); });
newSocket.on('connect', () => { socketRef.current = newSocket;
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);
});
setSocket(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 () => { return () => {
newSocket.close(); if (socketRef.current) {
console.log("Cleaning up socket connection...");
socketRef.current.disconnect();
socketRef.current.close();
socketRef.current = null;
}
}; };
}, []); }, []);
if (isLoading) {
return (
<div className="h-screen w-screen flex items-center justify-center">
<div className="text-4xl text-white">Connecting to socket...</div>
</div>
);
}
return ( return (
<SocketContext.Provider value={{ socket, isConnected }}> <SocketContext.Provider value={{ socket, isConnected }}>
{children} {children}
@@ -51,7 +104,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
export const useSocket = () => { export const useSocket = () => {
const context = useContext(SocketContext); const context = useContext(SocketContext);
if (context === undefined) { 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; return context;
}; };

View File

@@ -48,8 +48,15 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) {
title: stream.title, title: stream.title,
streamer: stream.username, streamer: stream.username,
viewers: stream.num_viewers, 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); setFeaturedStreams(extractedData);
}); });
@@ -57,15 +64,15 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) {
fetch(fetch_url[1]) fetch(fetch_url[1])
.then((response) => response.json()) .then((response) => response.json())
.then((data: CategoryItem[]) => { .then((data: CategoryItem[]) => {
const extractedData: CategoryItem[] = data.map( const extractedData: CategoryItem[] = data.map((category: any) => ({
(category: any) => ({ type: "category",
type: "category", id: category.category_id,
id: category.category_id, title: category.category_name,
title: category.category_name, viewers: category.num_viewers,
viewers: category.num_viewers, thumbnail: `/images/thumbnails/categories/${category.category_name
thumbnail: `/images/thumbnails/categories/${category.category_name.toLowerCase().replace(/ /g, "_")}.webp` .toLowerCase()
}) .replace(/ /g, "_")}.webp`,
); }));
console.log(extractedData); console.log(extractedData);
setFeaturedCategories(extractedData); setFeaturedCategories(extractedData);
}); });

View File

@@ -4,7 +4,7 @@ import './assets/styles/index.css'
import App from './App.tsx' import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> // <StrictMode>
<App /> <App />
</StrictMode>, // </StrictMode>,
) )

View File

@@ -23,24 +23,24 @@ interface StreamDataProps {
const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => { const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
const { isLoggedIn } = useAuth(); const { isLoggedIn } = useAuth();
const [showCheckout, setShowCheckout] = useState(false); // const [showCheckout, setShowCheckout] = useState(false);
const showReturn = window.location.search.includes("session_id"); const showReturn = window.location.search.includes("session_id");
const { streamerName } = useParams<{ streamerName: string }>(); const { streamerName } = useParams<{ streamerName: string }>();
const [streamData, setStreamData] = useState<StreamDataProps>(); const [streamData, setStreamData] = useState<StreamDataProps>();
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { // useEffect(() => {
// Prevent scrolling when checkout is open // // Prevent scrolling when checkout is open
if (showCheckout) { // if (showCheckout) {
document.body.style.overflow = "hidden"; // document.body.style.overflow = "hidden";
} else { // } else {
document.body.style.overflow = "unset"; // document.body.style.overflow = "unset";
} // }
// Cleanup function to ensure overflow is restored when component unmounts // // Cleanup function to ensure overflow is restored when component unmounts
return () => { // return () => {
document.body.style.overflow = "unset"; // document.body.style.overflow = "unset";
}; // };
}, [showCheckout]); // }, [showCheckout]);
useEffect(() => { useEffect(() => {
// Fetch stream data for this streamer // Fetch stream data for this streamer
fetch( fetch(
@@ -83,7 +83,7 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
> >
{isLoggedIn && ( {isLoggedIn && (
<Button <Button
onClick={() => setShowCheckout(true)} // onClick={() => setShowCheckout(true)}
extraClasses="mx-auto mb-4" extraClasses="mx-auto mb-4"
> >
Payment Screen Test Payment Screen Test
@@ -92,8 +92,8 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
</div> </div>
</div> </div>
{showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />} {/* {showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />} */}
{showReturn && <Return />} {/* {showReturn && <Return />} */}
</div> </div>
); );
}; };

View File

@@ -43,6 +43,23 @@ http {
listen 8080; listen 8080;
root /var/www; 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 # The MPEG-TS video chunks are stored in /tmp/hls
location ~ ^/stream/user/(.+\.ts)$ { location ~ ^/stream/user/(.+\.ts)$ {
alias /tmp/hls/$1; alias /tmp/hls/$1;
@@ -59,19 +76,6 @@ http {
expires -1d; 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 / { location / {
proxy_pass http://frontend:5173; # frontend is the name of the React container in docker-compose proxy_pass http://frontend:5173; # frontend is the name of the React container in docker-compose
} }

View File

@@ -16,4 +16,4 @@ COPY . .
ENV FLASK_APP=blueprints.__init__ ENV FLASK_APP=blueprints.__init__
ENV FLASK_DEBUG=True 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)"]

View File

@@ -1,7 +1,7 @@
from flask import Flask from flask import Flask
from flask_session import Session from flask_session import Session
from flask_cors import CORS 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 blueprints.errorhandlers import register_error_handlers
# from flask_wtf.csrf import CSRFProtect, generate_csrf # 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.stripe import stripe_bp
from blueprints.user import user_bp from blueprints.user import user_bp
from blueprints.streams import stream_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 from os import getenv
@@ -29,8 +30,11 @@ def create_app():
CORS(app, supports_credentials=True) CORS(app, supports_credentials=True)
# csrf.init_app(app) # csrf.init_app(app)
socketio.init_app(app)
Session(app) Session(app)
app.before_request(logged_in_user) app.before_request(logged_in_user)
app.after_request(record_time)
# adds in error handlers # adds in error handlers
register_error_handlers(app) register_error_handlers(app)
@@ -48,7 +52,6 @@ def create_app():
app.register_blueprint(stream_bp) app.register_blueprint(stream_bp)
app.register_blueprint(chat_bp) app.register_blueprint(chat_bp)
# Tell sockets where the initialisation app is socketio.init_app(app)
socketio.init_app(app, cors_allowed_origins="*")
return app return app

View File

@@ -1,11 +1,10 @@
from flask import Blueprint, jsonify, session from flask import Blueprint, jsonify, session
from database.database import Database 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 datetime import datetime
from flask_socketio import SocketIO
chat_bp = Blueprint("chat", __name__) chat_bp = Blueprint("chat", __name__)
socketio = SocketIO()
# <---------------------- ROUTES NEEDS TO BE CHANGED TO VIDEO OR DELETED AS DEEMED APPROPRIATE ----------------------> # <---------------------- ROUTES NEEDS TO BE CHANGED TO VIDEO OR DELETED AS DEEMED APPROPRIATE ---------------------->

View File

@@ -0,0 +1,3 @@
from flask_socketio import SocketIO
socketio = SocketIO(cors_allowed_origins="*", async_mode='gevent', logger=True, engineio_logger=True)

View File

@@ -120,7 +120,7 @@ def get_following_categories_streams():
""" """
Returns popular streams in categories which the user followed 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) return jsonify(streams)

View File

@@ -1,14 +1,24 @@
from flask import redirect, url_for, request, g, session from flask import redirect, url_for, request, g, session
from functools import wraps from functools import wraps
from re import match from re import match
from time import time
def logged_in_user(): def logged_in_user():
""" """
Validator to make sure a user is logged in. Validator to make sure a user is logged in.
""" """
g.start_time = time()
g.user = session.get("username", None) g.user = session.get("username", None)
g.admin = 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): def login_required(view):
""" """
Add at start of routes where users need to be logged in to access. Add at start of routes where users need to be logged in to access.

Binary file not shown.

View File

@@ -47,7 +47,8 @@ INSERT INTO subscribes (user_id, subscribed_id, since, expires) VALUES
(5, 105, '2024-08-30', '2025-02-28'); (5, 105, '2024-08-30', '2025-02-28');
INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio) VALUES 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 INSERT INTO chat (stream_id, chatter_id, message) VALUES
(1, 'Susan', 'Hey Every, loving the stream'), (1, 'Susan', 'Hey Every, loving the stream'),
@@ -67,17 +68,4 @@ SELECT * FROM stream_tags;
-- To see all tables in the database -- To see all tables in the database
SELECT name FROM sqlite_master WHERE type='table'; SELECT name FROM sqlite_master WHERE type='table';
INSERT INTO users
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

View File

@@ -1,6 +1,3 @@
-- View all tables in the database
SELECT name FROM sqlite_master WHERE type='table';
DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS users;
CREATE TABLE users CREATE TABLE users
( (

View File

@@ -21,4 +21,6 @@ typing_extensions==4.12.2
urllib3==2.3.0 urllib3==2.3.0
Werkzeug==3.1.3 Werkzeug==3.1.3
WTForms==3.2.1 WTForms==3.2.1
Gunicorn==20.1.0 Gunicorn==20.1.0
gevent>=22.10.2
gevent-websocket

View File

@@ -15,20 +15,22 @@ def user_recommendation_category(user_id: int) -> Optional[int]:
""", (user_id,)) """, (user_id,))
return data 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 Returns top 25 streams given a users category following
""" """
with Database() as db: with Database() as db:
categories = db.fetchall(""" categories = db.fetchall("""
SELECT users.user_id, title, username, num_viewers, category_name SELECT user_id, title, num_viewers, categories.category_name
FROM streams FROM streams
WHERE category_id IN (SELECT category_id FROM categories WHERE user_id = ?) JOIN categories ON streams.category_id = categories.category_id
ORDER BY num_viewers DESC WHERE category_id IN (SELECT category_id FROM categories WHERE user_id = ?)
LIMIT 25; ORDER BY num_viewers DESC
""", (user_id,)) LIMIT 25; """, (user_id,))
return categories 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]]]: 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 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: with Database() as db:
data = db.fetchall(""" data = db.fetchall("""
SELECT users.user_id, title, username, num_viewers, category_name SELECT streams.category_id, streams.user_id, streams.title, users.username, streams.num_viewers, categories.category_name
FROM streams FROM streams
JOIN users ON users.user_id = streams.user_id JOIN users ON users.user_id = streams.user_id
JOIN categories ON streams.category_id = categories.category_id JOIN categories ON streams.category_id = categories.category_id
WHERE categories.category_id = ? WHERE categories.category_id = ?
ORDER BY num_viewers DESC ORDER BY num_viewers DESC
LIMIT 25 LIMIT 25
""", (category_id,)) """, (category_id,))
return data return data
def default_recommendations(): def default_recommendations():
@@ -53,13 +55,13 @@ def default_recommendations():
""" """
with Database() as db: with Database() as db:
data = db.fetchall(""" data = db.fetchall("""
SELECT users.user_id, title, username, num_viewers, category_name SELECT stream_id, users.user_id, title, username, num_viewers, category_name
FROM streams FROM streams
JOIN users ON users.user_id = streams.user_id JOIN users ON users.user_id = streams.user_id
JOIN categories ON streams.category_id = categories.category_id JOIN categories ON streams.category_id = categories.category_id
ORDER BY num_viewers DESC ORDER BY num_viewers DESC
LIMIT 25; LIMIT 25;
""") """)
return data return data
def category_recommendations(): def category_recommendations():