Merge branch 'main' of https://github.com/john-david3/cs3305-team11
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
export const paths = {
|
|
||||||
pfps: "",
|
|
||||||
category_thumbnails: "",
|
|
||||||
icons: "",
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.0",
|
"version": "0.15.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --config vite.config.dev.ts",
|
"dev": "vite --config vite.config.dev.ts",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ interface VodListItemProps extends BaseListItemProps, Omit<VodType, "type"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VodListItem: React.FC<VodListItemProps> = ({
|
const VodListItem: React.FC<VodListItemProps> = ({
|
||||||
|
vod_id,
|
||||||
title,
|
title,
|
||||||
username,
|
username,
|
||||||
category_name,
|
category_name,
|
||||||
@@ -137,7 +138,7 @@ const VodListItem: React.FC<VodListItemProps> = ({
|
|||||||
|
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<h3 className="font-semibold text-lg text-white truncate max-w-full">{title}</h3>
|
<h3 className="font-semibold text-lg text-white truncate max-w-full">{title}</h3>
|
||||||
<p className="text-sm text-gray-300">{username}</p>
|
{variant != "vodDashboard" && <p className="text-sm text-gray-300">{username}</p>}
|
||||||
<p className="text-sm text-gray-400">{category_name}</p>
|
<p className="text-sm text-gray-400">{category_name}</p>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
<p className="text-xs text-gray-500">{datetime}</p>
|
<p className="text-xs text-gray-500">{datetime}</p>
|
||||||
@@ -147,20 +148,21 @@ const VodListItem: React.FC<VodListItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{variant === "vodDashboard" && (
|
{variant === "vodDashboard" && (
|
||||||
<div className="flex justify-evenly items-stretch rounded-b-lg">
|
<div className="flex justify-evenly items-stretch rounded-b-lg">
|
||||||
<button
|
{/* <button
|
||||||
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
|
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
|
||||||
onClick={() => console.log("Publish")}
|
onClick={() => console.log("Publish")}
|
||||||
>
|
>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
Publish
|
Publish
|
||||||
</button>
|
</button> */}
|
||||||
<button
|
<a
|
||||||
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
|
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
|
||||||
onClick={() => console.log("Download")}
|
href={`/vods/${username}/${vod_id}.mp4`}
|
||||||
|
download={`${username}_vod_${vod_id}.mp4`}
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
Download
|
Download
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ const ListRow = forwardRef<ListRowRef, ListRowProps>((props, ref) => {
|
|||||||
username={item.username}
|
username={item.username}
|
||||||
isLive={item.isLive}
|
isLive={item.isLive}
|
||||||
viewers={item.viewers}
|
viewers={item.viewers}
|
||||||
thumbnail={item.thumbnail}
|
|
||||||
onItemClick={() => onItemClick(item.username)}
|
onItemClick={() => onItemClick(item.username)}
|
||||||
extraClasses={itemExtraClasses}
|
extraClasses={itemExtraClasses}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses = "" }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${sidebarId.current}-category-${category.category_id}`}
|
key={`${sidebarId.current}-category-${category.category_id}`}
|
||||||
className="group relative flex flex-col items-center justify-center h-full max-h-[50px] border border-[--text-color]
|
className="group relative flex flex-col items-center justify-center w-full h-full max-h-[50px] border border-[--text-color]
|
||||||
rounded-lg overflow-hidden hover:shadow-lg transition-all text-white hover:text-purple-500 cursor-pointer"
|
rounded-lg overflow-hidden hover:shadow-lg transition-all text-white hover:text-purple-500 cursor-pointer"
|
||||||
onClick={() => (window.location.href = `/category/${category.category_name}`)}
|
onClick={() => (window.location.href = `/category/${category.category_name}`)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -225,13 +225,13 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
|||||||
const handleEndStream = async () => {
|
const handleEndStream = async () => {
|
||||||
console.log("Ending stream...");
|
console.log("Ending stream...");
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("key", streamData.stream_key);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/end_stream", {
|
const response = await fetch("/api/end_stream", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key: streamData.stream_key }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -375,7 +375,9 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
|||||||
streamCategory={streamData.category_name || "Category"}
|
streamCategory={streamData.category_name || "Category"}
|
||||||
viewers={streamData.viewer_count}
|
viewers={streamData.viewer_count}
|
||||||
thumbnail={thumbnailPreview.url || ""}
|
thumbnail={thumbnailPreview.url || ""}
|
||||||
onItemClick={() => {}}
|
onItemClick={() => {
|
||||||
|
window.open(`/${username}`, "_blank");
|
||||||
|
}}
|
||||||
extraClasses="max-w-[20vw]"
|
extraClasses="max-w-[20vw]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const processVodData = (data: any[]): VodType[] => {
|
|||||||
|
|
||||||
// Helper function to process API data into our consistent types
|
// Helper function to process API data into our consistent types
|
||||||
const processStreamData = (data: any[]): StreamType[] => {
|
const processStreamData = (data: any[]): StreamType[] => {
|
||||||
|
if (!data || data.length === 0 || !data[0] || !data[0].user_id) return [];
|
||||||
return data.map((stream) => ({
|
return data.map((stream) => ({
|
||||||
type: "stream",
|
type: "stream",
|
||||||
id: stream.user_id,
|
id: stream.user_id,
|
||||||
@@ -76,8 +77,9 @@ export function useFetchContent<T>(
|
|||||||
throw new Error(`Error fetching data: ${response.status}`);
|
throw new Error(`Error fetching data: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawData = await response.json();
|
const rawData = await response.json();
|
||||||
const processedData = processor(rawData);
|
let processedData = processor(Array.isArray(rawData) ? rawData : (rawData ? [rawData] : []));
|
||||||
|
console.log("processedData", processedData);
|
||||||
setData(processedData);
|
setData(processedData);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -126,7 +128,7 @@ export function useVods(customUrl?: string): {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
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<VodType>(url, processVodData, [customUrl]);
|
const { data, isLoading, error } = useFetchContent<VodType>(url, processVodData, [customUrl]);
|
||||||
|
|
||||||
return { vods: data, isLoading, error };
|
return { vods: data, isLoading, error };
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import AuthModal from "../components/Auth/AuthModal";
|
import AuthModal from "../components/Auth/AuthModal";
|
||||||
import { useAuthModal } from "../hooks/useAuthModal";
|
import { useAuthModal } from "../hooks/useAuthModal";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
@@ -10,8 +10,8 @@ import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
|||||||
import LoadingScreen from "../components/Layout/LoadingScreen";
|
import LoadingScreen from "../components/Layout/LoadingScreen";
|
||||||
import { StreamListItem } from "../components/Layout/ListItem";
|
import { StreamListItem } from "../components/Layout/ListItem";
|
||||||
import { EditIcon } from "lucide-react";
|
import { EditIcon } from "lucide-react";
|
||||||
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
import ListRow from "../components/Layout/ListRow";
|
||||||
import { useSameUser } from "../hooks/useSameUser";
|
import { useStreams, useVods } from "../hooks/useContent";
|
||||||
|
|
||||||
interface UserProfileData {
|
interface UserProfileData {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -20,22 +20,38 @@ interface UserProfileData {
|
|||||||
followerCount: number;
|
followerCount: number;
|
||||||
isPartnered: boolean;
|
isPartnered: boolean;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
currentStreamTitle?: string;
|
|
||||||
currentStreamCategory?: string;
|
|
||||||
currentStreamViewers?: number;
|
|
||||||
currentStreamStartTime?: string;
|
|
||||||
currentStreamThumbnail?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserPage: React.FC = () => {
|
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<UserProfileData>();
|
const [profileData, setProfileData] = useState<UserProfileData>();
|
||||||
const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow();
|
const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow();
|
||||||
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||||
const { username: loggedInUsername } = useAuth();
|
const { username: loggedInUsername } = useAuth();
|
||||||
const { username } = useParams();
|
const { username } = useParams();
|
||||||
const isUser = useSameUser({ username });
|
const { vods } = useVods(`/api/vods/${username}`);
|
||||||
const navigate = useNavigate();
|
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
|
// Saves uploaded image as profile picture for the user
|
||||||
const saveUploadedImage = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const saveUploadedImage = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -62,58 +78,20 @@ const UserPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if the current user is the currently logged-in user
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch user profile data
|
if (username === loggedInUsername) setUserPageVariant("personal");
|
||||||
fetch(`/api/user/${username}`)
|
// else if (data.isAdmin) setUserPageVariant("admin");
|
||||||
.then((res) => res.json())
|
else setUserPageVariant("user");
|
||||||
.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 (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);
|
if (loggedInUsername && username) checkFollowStatus(username);
|
||||||
}, [username]);
|
}, [username, loggedInUsername, checkFollowStatus]);
|
||||||
|
|
||||||
|
// Fetch user profile data
|
||||||
|
useEffect(() => {
|
||||||
|
if (!username) return;
|
||||||
|
fetchProfileData();
|
||||||
|
}, [fetchProfileData]);
|
||||||
|
|
||||||
if (!profileData) return <LoadingScreen />;
|
if (!profileData) return <LoadingScreen />;
|
||||||
|
|
||||||
@@ -148,16 +126,21 @@ const UserPage: React.FC = () => {
|
|||||||
} inset-0 z-20`}
|
} inset-0 z-20`}
|
||||||
style={{ boxShadow: "var(--user-pfp-border-shadow)" }}
|
style={{ boxShadow: "var(--user-pfp-border-shadow)" }}
|
||||||
>
|
>
|
||||||
<label className={`w-full h-full ${isUser ? "group cursor-pointer" : ""} overflow-visible rounded-full`}>
|
<label
|
||||||
|
className={`w-full h-full ${userPageVariant === "personal" ? "group cursor-pointer" : ""} overflow-visible rounded-full`}
|
||||||
|
>
|
||||||
{/* If user is live then displays a live div */}
|
{/* If user is live then displays a live div */}
|
||||||
{profileData.isLive && (
|
{profileData.isLive ? (
|
||||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-[#ff0000] text-white text-sm font-bold py-1 sm:px-5 px-4 z-30 flex items-center justify-center rounded-tr-xl rounded-bl-xl rounded-tl-xl rounded-br-xl">
|
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-[#ff0000] text-white text-sm font-bold py-1 sm:px-5 px-4 z-30 flex items-center justify-center rounded-tr-xl rounded-bl-xl rounded-tl-xl rounded-br-xl">
|
||||||
LIVE
|
LIVE
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
src={`/user/${profileData.username}/profile_picture`}
|
src={`/user/${profileData.username}/profile_picture`}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
|
console.log("no error")
|
||||||
e.currentTarget.src = "/images/pfps/default.png";
|
e.currentTarget.src = "/images/pfps/default.png";
|
||||||
e.currentTarget.onerror = null;
|
e.currentTarget.onerror = null;
|
||||||
}}
|
}}
|
||||||
@@ -166,7 +149,7 @@ const UserPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* If current user is the profile user then allow profile picture swap */}
|
{/* If current user is the profile user then allow profile picture swap */}
|
||||||
{isUser && (
|
{userPageVariant === "personal" && (
|
||||||
<div className="absolute top-0 bottom-0 left-0 right-0 m-auto flex items-center justify-center opacity-0 z-50 group-hover:opacity-100 transition-opacity duration-200">
|
<div className="absolute top-0 bottom-0 left-0 right-0 m-auto flex items-center justify-center opacity-0 z-50 group-hover:opacity-100 transition-opacity duration-200">
|
||||||
<EditIcon size={75} className="text-white bg-black/50 p-1 rounded-3xl" />
|
<EditIcon size={75} className="text-white bg-black/50 p-1 rounded-3xl" />
|
||||||
<input type="file" className="hidden" onChange={saveUploadedImage} accept="image/*" />
|
<input type="file" className="hidden" onChange={saveUploadedImage} accept="image/*" />
|
||||||
@@ -181,34 +164,32 @@ const UserPage: React.FC = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Follower Count */}
|
{/* Follower Count */}
|
||||||
{userPageVariant === "streamer" && (
|
<div className="flex items-center space-x-2 mb-6">
|
||||||
<>
|
<span className="text-gray-400">{profileData.followerCount.toLocaleString()} followers</span>
|
||||||
<div className="flex items-center space-x-2 mb-6">
|
{profileData.isPartnered && <span className="bg-purple-600 text-white text-sm px-2 py-1 rounded">Partner</span>}
|
||||||
<span className="text-gray-400">{profileData.followerCount.toLocaleString()} followers</span>
|
</div>
|
||||||
{profileData.isPartnered && <span className="bg-purple-600 text-white text-sm px-2 py-1 rounded">Partner</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* (Un)Follow Button */}
|
{/* (Un)Follow Button */}
|
||||||
{!isFollowing ? (
|
{userPageVariant != "personal" ? (
|
||||||
<Button
|
!isFollowing ? (
|
||||||
extraClasses="w-full bg-purple-700 hover:bg-[#28005e]"
|
<Button
|
||||||
onClick={() => followUser(profileData.id, setShowAuthModal)}
|
extraClasses="w-full bg-purple-700 z-50 hover:bg-[#28005e]"
|
||||||
>
|
onClick={() => followUser(profileData.id, setShowAuthModal)}
|
||||||
Follow
|
>
|
||||||
</Button>
|
Follow
|
||||||
) : (
|
</Button>
|
||||||
<Button extraClasses="w-full bg-[#a80000] z-50" onClick={() => unfollowUser(profileData?.id, setShowAuthModal)}>
|
) : (
|
||||||
Unfollow
|
<Button extraClasses="w-full bg-[#a80000] z-50" onClick={() => unfollowUser(profileData?.id, setShowAuthModal)}>
|
||||||
</Button>
|
Unfollow
|
||||||
)}
|
</Button>
|
||||||
</>
|
)
|
||||||
|
) : (
|
||||||
|
""
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{/* Bio */}
|
||||||
id="settings"
|
<div className="col-span-1 bg-[var(--user-sideBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center">
|
||||||
className="col-span-1 bg-[var(--user-sideBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center"
|
|
||||||
>
|
|
||||||
{/* User Type (e.g., "USER") */}
|
{/* User Type (e.g., "USER") */}
|
||||||
<small className="text-green-400">{userPageVariant.toUpperCase()}</small>
|
<small className="text-green-400">{userPageVariant.toUpperCase()}</small>
|
||||||
|
|
||||||
@@ -223,44 +204,42 @@ const UserPage: React.FC = () => {
|
|||||||
id="content"
|
id="content"
|
||||||
className="col-span-2 bg-[var(--user-contentBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center"
|
className="col-span-2 bg-[var(--user-contentBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center"
|
||||||
>
|
>
|
||||||
{userPageVariant === "streamer" && (
|
{/* Stream */}
|
||||||
<>
|
{currentStream && (
|
||||||
{profileData.isLive ? (
|
<div className="mb-8">
|
||||||
<div className="mb-8">
|
<h2 className="text-2xl bg-[#ff0000] border py-4 px-12 font-black mb-4 rounded-[4rem]">Currently Live!</h2>
|
||||||
<h2 className="text-2xl bg-[#ff0000] border py-4 px-12 font-black mb-4 rounded-[4rem]">Currently Live!</h2>
|
<StreamListItem
|
||||||
<StreamListItem
|
id={profileData.id}
|
||||||
id={profileData.id}
|
title={currentStream.title || ""}
|
||||||
title={profileData.currentStreamTitle || ""}
|
streamCategory=""
|
||||||
streamCategory=""
|
username=""
|
||||||
username=""
|
viewers={currentStream.viewers || 0}
|
||||||
viewers={profileData.currentStreamViewers || 0}
|
thumbnail={currentStream.thumbnail}
|
||||||
thumbnail={profileData.currentStreamThumbnail}
|
onItemClick={() => {
|
||||||
onItemClick={() => {
|
navigate(`/${profileData.username}`);
|
||||||
navigate(`/${profileData.username}`);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<h1>Currently not live</h1>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ↓↓ VODS ↓↓ */}
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Past Broadcasts</h2>
|
|
||||||
<div className="text-gray-400 rounded-none">No past broadcasts found</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
{/* VODs */}
|
||||||
{userPageVariant === "user" && (
|
{vods.length > 0 && (
|
||||||
<>
|
<div>
|
||||||
{/* ↓↓ VODS ↓↓ */}
|
<h2 className="text-2xl font-bold mb-4"></h2>
|
||||||
<div>
|
<ListRow
|
||||||
<h2 className="text-2xl font-bold mb-4">Past Broadcasts</h2>
|
type="vod"
|
||||||
<div className="text-gray-400 rounded-none">No past broadcasts found</div>
|
title={`Past Broadcasts (${vods.length})`}
|
||||||
</div>
|
items={vods}
|
||||||
</>
|
onItemClick={(vod) => {
|
||||||
|
console.log("VOD Clicked:", vod);
|
||||||
|
}}
|
||||||
|
extraClasses="w-fit max-w-[40vw] py-0 mt-0"
|
||||||
|
amountForScroll={2}
|
||||||
|
itemExtraClasses="w-[15vw]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* No Content */}
|
||||||
|
{vods.length === 0 && currentStream && <h2 className="text-2xl font-bold mb-4">No Content Made Yet</h2>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -294,7 +273,7 @@ const UserPage: React.FC = () => {
|
|||||||
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "var(--follow-shadow)")}
|
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "var(--follow-shadow)")}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
||||||
>
|
>
|
||||||
<button onClick={() => navigate(`/user/${username}/yourCategories`)}>Categories</button>
|
<button onClick={() => navigate(`/user/${username}/followedCategories`)}>Categories</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ export interface UserType {
|
|||||||
username: string;
|
username: string;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
viewers: number;
|
viewers: number;
|
||||||
thumbnail?: string;
|
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,11 @@ export default defineConfig({
|
|||||||
target: "http://localhost:8080",
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
"/stream": {
|
"/stream": {
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
"/images": {
|
"/images": {
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@@ -33,6 +33,10 @@ export default defineConfig({
|
|||||||
target: "http://localhost:8080",
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
"/vods": {
|
||||||
|
target: "http://localhost:8080",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -99,12 +99,12 @@ http {
|
|||||||
alias /user_data/$1/vods/$2;
|
alias /user_data/$1/vods/$2;
|
||||||
# where $1 is the user's username and $2 is the thumbnail_name
|
# where $1 is the user's username and $2 is the thumbnail_name
|
||||||
|
|
||||||
# The thumbnails should not be cacheable
|
# The thumbnails should not be cacheable
|
||||||
expires -1d;
|
expires -1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
## Profile pictures location
|
## Profile pictures location
|
||||||
location ~ ^/user/(.+)/index.png$ {
|
location ~ ^/user/(.+)/profile_picture$ {
|
||||||
alias /user_data/$1/index.png;
|
alias /user_data/$1/index.png;
|
||||||
# where $1 is the user's username
|
# where $1 is the user's username
|
||||||
|
|
||||||
|
|||||||
@@ -95,12 +95,6 @@ def signup():
|
|||||||
|
|
||||||
# Create user directories for stream data
|
# Create user directories for stream data
|
||||||
path_manager.create_user(username)
|
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)
|
# send_email(username)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -178,8 +172,11 @@ def login():
|
|||||||
"error_fields": ["username", "password"],
|
"error_fields": ["username", "password"],
|
||||||
"message": "Invalid username or password"
|
"message": "Invalid username or password"
|
||||||
}), 401
|
}), 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.clear()
|
||||||
session["username"] = username
|
session["username"] = username
|
||||||
session["user_id"] = get_user_id(username)
|
session["user_id"] = get_user_id(username)
|
||||||
@@ -209,8 +206,27 @@ def logout() -> dict:
|
|||||||
"""
|
"""
|
||||||
Log out and clear the users session.
|
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.
|
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()
|
session.clear()
|
||||||
return {"logged_in": False}
|
return {"logged_in": False}
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,17 @@ def recommended_streams() -> list[dict]:
|
|||||||
return streams
|
return streams
|
||||||
|
|
||||||
|
|
||||||
|
@stream_bp.route('/streams/<string:username>/data')
|
||||||
@stream_bp.route('/streams/<int:streamer_id>/data')
|
@stream_bp.route('/streams/<int:streamer_id>/data')
|
||||||
def stream_data(streamer_id) -> dict:
|
def stream_data(username=None, streamer_id=None) -> dict:
|
||||||
"""
|
"""
|
||||||
Returns a streamer's current stream data
|
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)
|
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:
|
if session.get('user_id') == streamer_id:
|
||||||
with Database() as db:
|
with Database() as db:
|
||||||
stream_key = db.fetchone(
|
stream_key = db.fetchone(
|
||||||
@@ -112,33 +116,21 @@ def recommended_categories() -> list | list[dict]:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
user_id = session.get("user_id")
|
user_id = session.get("user_id")
|
||||||
categories = get_user_category_recommendations(1)
|
categories = get_user_category_recommendations(user_id)
|
||||||
return jsonify(categories)
|
return jsonify(categories)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@stream_bp.route('/categories/following')
|
@stream_bp.route('/streams/followed_categories')
|
||||||
def following_categories_streams():
|
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'))
|
streams = get_followed_categories_recommendations(session.get('user_id'))
|
||||||
return jsonify(streams)
|
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
|
# User Routes
|
||||||
@stream_bp.route('/user/<string:username>/status')
|
@stream_bp.route('/user/<string:username>/status')
|
||||||
def user_live_status(username):
|
def user_live_status(username):
|
||||||
@@ -172,7 +164,7 @@ def user_live_status(username):
|
|||||||
@stream_bp.route('/vods/<int:vod_id>')
|
@stream_bp.route('/vods/<int:vod_id>')
|
||||||
def vod(vod_id):
|
def vod(vod_id):
|
||||||
"""
|
"""
|
||||||
Returns a JSON of a vod
|
Returns details about a specific vod
|
||||||
"""
|
"""
|
||||||
vod = get_vod(vod_id)
|
vod = get_vod(vod_id)
|
||||||
return jsonify(vod)
|
return jsonify(vod)
|
||||||
@@ -187,6 +179,7 @@ def vods(username):
|
|||||||
"vod_id": int,
|
"vod_id": int,
|
||||||
"title": str,
|
"title": str,
|
||||||
"datetime": str,
|
"datetime": str,
|
||||||
|
"username": str,
|
||||||
"category_name": str,
|
"category_name": str,
|
||||||
"length": int (in seconds),
|
"length": int (in seconds),
|
||||||
"views": int
|
"views": int
|
||||||
@@ -204,10 +197,8 @@ def get_all_vods():
|
|||||||
Returns data of all VODs by all streamers in a JSON-compatible format
|
Returns data of all VODs by all streamers in a JSON-compatible format
|
||||||
"""
|
"""
|
||||||
with Database() as db:
|
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;""")
|
||||||
|
|
||||||
print("Fetched VODs from DB:", vods)
|
|
||||||
|
|
||||||
return jsonify(vods)
|
return jsonify(vods)
|
||||||
|
|
||||||
# RTMP Server Routes
|
# RTMP Server Routes
|
||||||
@@ -355,23 +346,12 @@ def update_stream():
|
|||||||
@stream_bp.route("/end_stream", methods=["POST"])
|
@stream_bp.route("/end_stream", methods=["POST"])
|
||||||
def end_stream():
|
def end_stream():
|
||||||
"""
|
"""
|
||||||
Ends a stream
|
Ends a stream based on the HTTP request
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
|
print("Ending stream", flush=True)
|
||||||
|
|
||||||
print("TEST END STREAM")
|
|
||||||
stream_key = request.get_json().get("key")
|
stream_key = request.get_json().get("key")
|
||||||
print(stream_key, flush=True)
|
|
||||||
user_id = None
|
|
||||||
username = None
|
|
||||||
|
|
||||||
if not stream_key:
|
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)
|
# 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")
|
stream_key = request.form.get("name")
|
||||||
@@ -380,60 +360,24 @@ def end_stream():
|
|||||||
print("Unauthorized - No stream key provided", flush=True)
|
print("Unauthorized - No stream key provided", flush=True)
|
||||||
return "Unauthorized", 403
|
return "Unauthorized", 403
|
||||||
|
|
||||||
# Open database connection
|
# Get user info from stream key
|
||||||
with Database() as db:
|
with Database() as db:
|
||||||
initial_streams = db.fetchall("""SELECT title FROM streams""")
|
user_info = db.fetchone("""SELECT user_id, username
|
||||||
print("Initial streams:", initial_streams, flush=True)
|
|
||||||
|
|
||||||
# Get user info from stream key
|
|
||||||
user_info = db.fetchone("""SELECT *
|
|
||||||
FROM users
|
FROM users
|
||||||
WHERE stream_key = ?""", (stream_key,))
|
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:
|
if not user_info:
|
||||||
print("Unauthorized - No user found from stream key", flush=True)
|
print("Unauthorized - No user found from stream key", flush=True)
|
||||||
return "Unauthorized", 403
|
return "Unauthorized", 403
|
||||||
# If stream never published, return
|
|
||||||
if not stream_info:
|
user_id = user_info["user_id"]
|
||||||
print(f"Stream for stream key: {stream_key} never began", flush=True)
|
username = user_info["username"]
|
||||||
return "Stream ended", 200
|
|
||||||
|
result, message = end_user_stream(stream_key, user_id, username)
|
||||||
# Remove stream from database
|
|
||||||
db.execute("""DELETE FROM streams
|
if result:
|
||||||
WHERE user_id = ?""", (user_id,))
|
print(f"Stream ended: {message}", flush=True)
|
||||||
|
return "Stream ended", 200
|
||||||
# Move stream to vod table
|
else:
|
||||||
stream_length = int(
|
print(f"Error ending stream: {message}", flush=True)
|
||||||
(datetime.now() - parser.parse(stream_info.get("start_time"))).total_seconds())
|
return "Error ending stream", 500
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -61,17 +61,6 @@ def user_profile_picture_save():
|
|||||||
|
|
||||||
return jsonify({"message": "Profile picture saved", "path": thumbnail_path})
|
return jsonify({"message": "Profile picture saved", "path": thumbnail_path})
|
||||||
|
|
||||||
@login_required
|
|
||||||
@user_bp.route('/user/same/<string:username>')
|
|
||||||
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
|
## Subscription Routes
|
||||||
@login_required
|
@login_required
|
||||||
@user_bp.route('/user/subscription/<string:streamer_name>')
|
@user_bp.route('/user/subscription/<string:streamer_name>')
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def get_highest_view_categories(no_categories: int = 4, offset: int = 0) -> Opti
|
|||||||
""", (no_categories, offset))
|
""", (no_categories, offset))
|
||||||
return categories
|
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
|
Queries user_preferences database to find users top favourite streaming category and returns the category
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -48,6 +48,81 @@ def get_current_stream_data(user_id: int) -> Optional[dict]:
|
|||||||
""", (user_id,))
|
""", (user_id,))
|
||||||
return most_recent_stream
|
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()
|
||||||
|
print(f"Ending stream for user {username} (ID: {user_id})", flush=True)
|
||||||
|
|
||||||
|
if not stream_key or not user_id or not username:
|
||||||
|
print("Cannot end stream - missing required information", flush=True)
|
||||||
|
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:
|
||||||
|
print(f"User {username} (ID: {user_id}) is not streaming", flush=True)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Stream ended for user {username} (ID: {user_id})", flush=True)
|
||||||
|
return True, "Stream ended successfully"
|
||||||
|
|
||||||
|
except Exception as 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]:
|
def get_category_id(category_name: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Returns the category_id given a category name
|
Returns the category_id given a category name
|
||||||
@@ -77,7 +152,7 @@ def get_vod(vod_id: int) -> dict:
|
|||||||
Returns data of a streamers vod
|
Returns data of a streamers vod
|
||||||
"""
|
"""
|
||||||
with Database() as db:
|
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
|
return vod
|
||||||
|
|
||||||
def get_latest_vod(user_id: int):
|
def get_latest_vod(user_id: int):
|
||||||
@@ -85,7 +160,7 @@ def get_latest_vod(user_id: int):
|
|||||||
Returns data of the most recent stream by a streamer
|
Returns data of the most recent stream by a streamer
|
||||||
"""
|
"""
|
||||||
with Database() as db:
|
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
|
return latest_vod
|
||||||
|
|
||||||
def get_user_vods(user_id: int):
|
def get_user_vods(user_id: int):
|
||||||
@@ -93,15 +168,7 @@ def get_user_vods(user_id: int):
|
|||||||
Returns data of all vods by a streamer
|
Returns data of all vods by a streamer
|
||||||
"""
|
"""
|
||||||
with Database() as db:
|
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,))
|
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 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""")
|
|
||||||
return vods
|
return vods
|
||||||
|
|
||||||
def generate_thumbnail(stream_file: str, thumbnail_file: str) -> None:
|
def generate_thumbnail(stream_file: str, thumbnail_file: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user