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] 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: