From 6f449eea4e73258dea1685224bbec6aa909866a2 Mon Sep 17 00:00:00 2001 From: ThisBirchWood Date: Fri, 28 Feb 2025 19:38:03 +0000 Subject: [PATCH 01/17] 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 edb959506a35bf8515bd911b6512351f41e592a1 Mon Sep 17 00:00:00 2001 From: EvanLin3141 Date: Fri, 28 Feb 2025 19:52:01 +0000 Subject: [PATCH 02/17] ADD: All Vods on Home Page ToDo: Thumbnail in DB Save user stream to Vod --- frontend/src/components/Layout/ListItem.tsx | 51 ++++++++++++++++- frontend/src/components/Layout/ListRow.tsx | 28 ++++++++- frontend/src/hooks/useContent.ts | 38 +++++++++++++ frontend/src/pages/HomePage.tsx | 63 ++++++++++----------- frontend/src/types/VodType.ts | 12 ++++ 5 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 frontend/src/types/VodType.ts diff --git a/frontend/src/components/Layout/ListItem.tsx b/frontend/src/components/Layout/ListItem.tsx index a5a7cf8..dca0440 100644 --- a/frontend/src/components/Layout/ListItem.tsx +++ b/frontend/src/components/Layout/ListItem.tsx @@ -2,13 +2,14 @@ import React from "react"; import { StreamType } from "../../types/StreamType"; import { CategoryType } from "../../types/CategoryType"; import { UserType } from "../../types/UserType"; +import { VodType } from "../../types/VodType"; // Base props that all item types share interface BaseListItemProps { onItemClick?: () => void; extraClasses?: string; } - + // Stream item component interface StreamListItemProps extends BaseListItemProps, Omit {} @@ -124,6 +125,52 @@ const UserListItem: React.FC = ({ ); }; +// VODs item component +interface VodListItemProps extends BaseListItemProps, Omit {} + +const VodListItem: React.FC = ({ + title, + streamer, + datetime, + category, + length, + views, + thumbnail, + url, + onItemClick, + extraClasses = "", +}) => { + return ( +
+
window.open(url, "_blank")} + > +
+ {thumbnail ? ( + {title} + ) : ( +
+ )} +
+
+

+ {title} +

+

{streamer}

+

{category}

+

{new Date(datetime).toLocaleDateString()} | {length} mins

+

{views} views

+
+
+
+ ); +}; + // Legacy wrapper component for backward compatibility export interface ListItemProps { type: "stream" | "category" | "user"; @@ -138,4 +185,4 @@ export interface ListItemProps { isLive?: boolean; } -export { StreamListItem, CategoryListItem, UserListItem }; \ No newline at end of file +export { StreamListItem, CategoryListItem, UserListItem, VodListItem }; \ No newline at end of file diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index f61293a..0f7af82 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -10,16 +10,17 @@ import React, { } from "react"; import { useNavigate } from "react-router-dom"; import "../../assets/styles/listRow.css"; -import { StreamListItem, CategoryListItem, UserListItem } from "./ListItem"; +import { StreamListItem, CategoryListItem, UserListItem, VodListItem } from "./ListItem"; import { StreamType } from "../../types/StreamType"; import { CategoryType } from "../../types/CategoryType"; import { UserType } from "../../types/UserType"; +import { VodType } from "../../types/VodType" -type ItemType = StreamType | CategoryType | UserType; +type ItemType = StreamType | CategoryType | UserType | VodType; interface ListRowProps { variant?: "default" | "search"; - type: "stream" | "category" | "user"; + type: "stream" | "category" | "user" | "vod"; title?: string; description?: string; items: ItemType[]; @@ -100,6 +101,9 @@ const ListRow = forwardRef((props, ref) => { const isUserType = (item: ItemType): item is UserType => item.type === "user"; + const isVodType = (item: ItemType): item is VodType => + item.type === "vod"; + return (
((props, ref) => { /> ); } + else if (type === "vod" && isVodType(item)) { + return ( + window.open(item.url, "_blank")} + extraClasses={itemExtraClasses} + /> + ); + } return null; })} diff --git a/frontend/src/hooks/useContent.ts b/frontend/src/hooks/useContent.ts index 5e99aaf..d705885 100644 --- a/frontend/src/hooks/useContent.ts +++ b/frontend/src/hooks/useContent.ts @@ -4,8 +4,28 @@ import { useAuth } from "../context/AuthContext"; import { StreamType } from "../types/StreamType"; import { CategoryType } from "../types/CategoryType"; import { UserType } from "../types/UserType"; +import { VodType } from "../types/VodType" import { getCategoryThumbnail } from "../utils/thumbnailUtils"; +// Process API data into our VodType structure +const processVodData = (data: any[]): VodType[] => { + console.log("Raw API VOD Data:", data); // Debugging + return data.map((vod) => ({ + type: "vod", + id: vod.id, // Ensure this matches API response + title: vod.title, + streamer: vod.streamer, // Ensure backend sends streamer name or ID + datetime: new Date(vod.datetime).toLocaleString(), + category: vod.category, + length: vod.length, + views: vod.views, + url: vod.url, + thumbnail: "../../images/category_thumbnails/abstract.webp", + })); +}; + + + // Helper function to process API data into our consistent types const processStreamData = (data: any[]): StreamType[] => { return data.map((stream) => ({ @@ -118,6 +138,24 @@ export function useCategories(customUrl?: string): { return { categories: data, isLoading, error }; } +export function useVods(customUrl?: string): { + vods: VodType[]; + isLoading: boolean; + error: string | null +} { + const url = customUrl || "api/vods/all"; + const { data, isLoading, error } = useFetchContent( + url, + processVodData, + [customUrl] + ); + + console.log("Fetched VODs Data:", data); // Debugging + + return { vods: data, isLoading, error }; +} + + export function useUsers(customUrl?: string): { users: UserType[]; isLoading: boolean; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 8497eec..5b79a56 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -6,25 +6,25 @@ import Button from "../components/Input/Button"; import DynamicPageContent from "../components/Layout/DynamicPageContent"; import LoadingScreen from "../components/Layout/LoadingScreen"; import Footer from "../components/Layout/Footer"; +import { useVods } from "../hooks/useContent"; // Import useVods + interface HomePageProps { variant?: "default" | "personalised"; } + const HomePage: React.FC = ({ variant = "default" }) => { const { streams, isLoading: isLoadingStreams } = useStreams(); const { categories, isLoading: isLoadingCategories } = useCategories(); + const { vods, isLoading: isLoadingVods } = useVods(); // Fetch VODs const navigate = useNavigate(); - const handleStreamClick = (streamerName: string) => { - window.location.href = `/${streamerName}`; + const handleVodClick = (vodUrl: string) => { + window.open(vodUrl, "_blank"); // Open VOD in new tab }; - const handleCategoryClick = (categoryName: string) => { - navigate(`/category/${categoryName}`); - }; - - if (isLoadingStreams || isLoadingCategories) + if (isLoadingStreams || isLoadingCategories || isLoadingVods) return Loading Content...; return ( @@ -33,52 +33,47 @@ const HomePage: React.FC = ({ variant = "default" }) => { className="relative min-h-screen animate-moving_bg" contentClassName="pb-[12vh]" > + {/* Streams Section */} navigate(`/${streamerName}`)} extraClasses="bg-[var(--liveNow)]" itemExtraClasses="w-[20vw]" /> - {/* If Personalised_HomePage, display Categories the logged-in user follows. Else, trending categories. */} + {/* Categories Section */} navigate(`/category/${categoryName}`)} titleClickable={true} extraClasses="bg-[var(--recommend)]" itemExtraClasses="w-[20vw]" > - -