This commit is contained in:
EvanLin3141
2025-02-22 20:18:18 +00:00
13 changed files with 320 additions and 243 deletions

View File

@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" class="min-w-[650px]">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
@@ -7,7 +7,7 @@
<title>Team Software Project</title> <title>Team Software Project</title>
</head> </head>
<body> <body>
<div id="root" class="min-h-screen h-full bg-gradient-to-tr from-[#2043ba] via-[#0026a6] to-[#63007a] overflow-x-hidden"></div> <div id="root" class="min-h-screen h-full min-w-[650px] bg-gradient-to-tr from-[#2043ba] via-[#0026a6] to-[#63007a] overflow-x-hidden"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -148,7 +148,7 @@ const LoginForm: React.FC<SubmitProps> = ({ onSubmit, onForgotPassword }) => {
errors.password ? "border-red-500" : "" errors.password ? "border-red-500" : ""
}`} }`}
></Input> ></Input>
<div className="flex flex-row"> <div className="flex">
<label className="flex w-full items-center justify-start cursor-pointer w-10px"> <label className="flex w-full items-center justify-start cursor-pointer w-10px">
<input <input
type="checkbox" type="checkbox"

View File

@@ -28,7 +28,7 @@ const ListItem: React.FC<ListItemProps> = ({
className={`${extraClasses} overflow-hidden flex-shrink-0 flex flex-col bg-purple-900 rounded-lg cursor-pointer mx-auto hover:bg-pink-700 hover:scale-105 transition-all`} className={`${extraClasses} overflow-hidden flex-shrink-0 flex flex-col bg-purple-900 rounded-lg cursor-pointer mx-auto hover:bg-pink-700 hover:scale-105 transition-all`}
onClick={onItemClick} onClick={onItemClick}
> >
<div className="relative w-full pt-[56.25%] overflow-hidden rounded-t-lg"> <div className="relative w-full aspect-video overflow-hidden rounded-t-lg">
{thumbnail ? ( {thumbnail ? (
<img <img
src={thumbnail} src={thumbnail}

View File

@@ -73,7 +73,7 @@ const ListRow: React.FC<ListRowProps> = ({
ref={slider} ref={slider}
className={`flex ${ className={`flex ${
wrap ? "flex-wrap" : "overflow-x-scroll whitespace-nowrap" wrap ? "flex-wrap" : "overflow-x-scroll whitespace-nowrap"
} items-center justify-between scroll-smooth scrollbar-hide gap-5 py-[10px] px=[30px] mx-[30px]`} } max-w-[95%] items-center justify-between scroll-smooth scrollbar-hide gap-5 mx-auto`}
> >
{items.map((item) => ( {items.map((item) => (
@@ -93,7 +93,7 @@ const ListRow: React.FC<ListRowProps> = ({
? onClick?.(item.streamer) ? onClick?.(item.streamer)
: onClick?.(item.title) : onClick?.(item.title)
} }
extraClasses={`${itemExtraClasses} min-w-[25vw]`} extraClasses={`${itemExtraClasses} min-w-[20vw]`}
/> />
))} ))}
</div> </div>

View File

@@ -0,0 +1,117 @@
import React, { useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import videojs from "video.js";
import type Player from "video.js/dist/types/player";
import "video.js/dist/video-js.css";
interface VideoPlayerProps {
streamer?: string;
extraClasses?: string;
onStreamDetected?: (isStreamAvailable: boolean) => void;
}
const VideoPlayer: React.FC<VideoPlayerProps> = ({
streamer,
extraClasses,
onStreamDetected,
}) => {
const { streamerName: urlStreamerName } = useParams<{
streamerName: string;
}>();
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
// Use URL param if available, otherwise fall back to prop
const streamerName = urlStreamerName || streamer;
useEffect(() => {
if (!videoRef.current || !streamerName) {
console.log("No video ref or streamer name");
return;
}
const setupPlayer = async () => {
const streamKey = await fetchStreamKey();
const streamUrl = `/stream/${streamKey}/index.m3u8`;
console.log("Player created with src:", streamUrl);
if (!playerRef.current) {
const videoElement = document.createElement("video");
videoElement.classList.add(
"video-js",
"vjs-big-play-centered",
"w-full",
"h-full"
);
videoElement.setAttribute("playsinline", "true");
if (videoRef.current) {
videoRef.current.appendChild(videoElement);
}
playerRef.current = videojs(videoElement, {
controls: false,
autoplay: true,
muted: false,
fluid: true,
responsive: true,
aspectRatio: "16:9",
liveui: false,
sources: [
{
src: streamUrl,
type: "application/x-mpegURL",
},
],
});
playerRef.current.on("loadeddata", () => {
if (onStreamDetected) onStreamDetected(true);
});
playerRef.current.on("error", () => {
console.error(`Stream failed to load: ${streamUrl}`);
if (onStreamDetected) onStreamDetected(false);
setTimeout(() => {
console.log("Retrying stream...");
playerRef.current?.src({
src: streamUrl,
type: "application/x-mpegURL",
});
playerRef.current?.play();
}, 5000);
});
} else {
playerRef.current.src({
src: streamUrl,
type: "application/x-mpegURL",
});
playerRef.current.play();
}
};
const fetchStreamKey = async () => {
const response = await fetch(`/api/user/${streamerName}/stream_key`);
const keyData = await response.json();
return keyData.stream_key;
};
setupPlayer();
return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [streamerName]);
return (
<div
id="video-player"
className={`${extraClasses} w-full h-full mx-auto flex justify-center items-center bg-gray-900 rounded-lg`}
>
<div ref={videoRef} className="w-full max-w-[160vh] self-center" />
</div>
);
};
export default VideoPlayer;

View File

@@ -1,96 +0,0 @@
import React, { useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import videojs from "video.js";
import "video.js/dist/video-js.css";
interface VideoPlayerProps {
streamer?: string;
extraClasses?: string;
onStreamDetected?: (isStreamAvailable: boolean) => void;
}
const VideoPlayer: React.FC<VideoPlayerProps> = ({
streamer,
extraClasses,
onStreamDetected,
}) => {
const { streamerName: urlStreamerName } = useParams<{
streamerName: string;
}>();
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<videojs.Player | null>(null);
// Use URL param if available, otherwise fall back to prop
const streamerName = urlStreamerName || streamer;
useEffect(() => {
if (!videoRef.current || !streamerName) return;
const streamUrl = `/stream/${streamerName}/index.m3u8`;
if (!playerRef.current) {
const videoElement = document.createElement("video");
videoElement.classList.add(
"video-js",
"vjs-big-play-centered",
"w-full",
"h-full"
);
videoElement.setAttribute("playsinline", "true");
videoRef.current.appendChild(videoElement);
playerRef.current = videojs(videoElement, {
controls: false,
autoplay: true,
muted: false,
fluid: true,
responsive: true,
aspectRatio: "16:9",
liveui: false,
sources: [
{
src: streamUrl,
type: "application/x-mpegURL",
},
],
});
playerRef.current.on('loadeddata', () => {
if (onStreamDetected) onStreamDetected(true);
});
playerRef.current.on("error", () => {
console.error(`Stream failed to load: ${streamUrl}`);
if (onStreamDetected) onStreamDetected(false);
setTimeout(() => {
console.log("Retrying stream...");
playerRef.current?.src({
src: streamUrl,
type: "application/x-mpegURL",
});
playerRef.current?.play();
}, 5000);
});
} else {
playerRef.current.src({ src: streamUrl, type: "application/x-mpegURL" });
playerRef.current.play();
}
return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [streamerName]);
return (
<div
id="video-player"
className={`${extraClasses} w-full h-full mx-auto flex justify-center items-center bg-gray-900 rounded-lg`}
>
<div ref={videoRef} className="w-full max-w-[160vh] self-center" />
</div>
);
};
export default VideoPlayer;

View File

@@ -44,10 +44,9 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
wrap={false} wrap={false}
onClick={handleStreamClick} onClick={handleStreamClick}
extraClasses="bg-[var(--liveNow)]" extraClasses="bg-[var(--liveNow)]"
> >
{/* <Button extraClasses="absolute right-10" onClick={() => navigate("/")}> {/* <Button extraClasses="absolute right-10" onClick={() => navigate("/")}>
Show More . . . Show More
</Button> */} </Button> */}
</ListRow> </ListRow>
@@ -73,7 +72,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
extraClasses="absolute right-10" extraClasses="absolute right-10"
onClick={() => navigate("/categories")} onClick={() => navigate("/categories")}
> >
Show More . . . Show More
</Button> </Button>
</ListRow> </ListRow>
</DynamicPageContent> </DynamicPageContent>

View File

@@ -6,7 +6,7 @@ import ListItem from "../components/Layout/ListItem";
import { X as XIcon, Eye as ShowIcon, EyeOff as HideIcon } from "lucide-react"; import { X as XIcon, Eye as ShowIcon, EyeOff as HideIcon } from "lucide-react";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import { debounce } from "lodash"; import { debounce } from "lodash";
import VideoPlayer from "../components/Video/VideoPlayer"; import VideoPlayer from "../components/Stream/VideoPlayer";
interface StreamData { interface StreamData {
title: string; title: string;
@@ -66,71 +66,70 @@ const StreamDashboardPage: React.FC = () => {
}, [categories, thumbnailPreview.isCustom]); }, [categories, thumbnailPreview.isCustom]);
useEffect(() => { useEffect(() => {
const checkStreamStatus = async () => {
try {
const response = await fetch(`/api/user/${username}/status`);
const data = await response.json();
setIsStreaming(data.is_live);
if (data.is_live) {
const streamResponse = await fetch(
`/api/streams/${data.user_id}/data`,
{ credentials: "include" }
);
const streamData = await streamResponse.json();
setStreamData({
title: streamData.title,
category_name: streamData.category_name,
viewer_count: streamData.num_viewers,
start_time: streamData.start_time,
stream_key: streamData.stream_key,
});
console.log("Stream data:", streamData);
const time = Math.floor(
(Date.now() - new Date(streamData.start_time).getTime()) / 60000 // Convert to minutes
);
if (time < 60) setTimeStarted(`${time}m ago`);
else if (time < 1440)
setTimeStarted(`${Math.floor(time / 60)}h ${time % 60}m ago`);
else
setTimeStarted(
`${Math.floor(time / 1440)}d ${Math.floor((time % 1440) / 60)}h ${
time % 60
}m ago`
);
} else {
const response = await fetch(`/api/user/${username}/stream_key`);
const keyData = await response.json();
setStreamData((prev) => ({
...prev,
stream_key: keyData.stream_key,
}));
console.log("Stream key:", keyData.stream_key);
}
} catch (error) {
console.error("Error checking stream status:", error);
}
};
const fetchCategories = async () => {
try {
const response = await fetch("/api/categories/popular/100");
const data = await response.json();
setCategories(data);
setFilteredCategories(data);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
checkStreamStatus(); checkStreamStatus();
fetchCategories(); fetchCategories();
}, [username]); }, [username]);
const checkStreamStatus = async () => {
try {
const response = await fetch(`/api/user/${username}/status`);
const data = await response.json();
setIsStreaming(data.is_live);
if (data.is_live) {
const streamResponse = await fetch(
`/api/streams/${data.user_id}/data`,
{ credentials: "include" }
);
const streamData = await streamResponse.json();
setStreamData({
title: streamData.title,
category_name: streamData.category_name,
viewer_count: streamData.num_viewers,
start_time: streamData.start_time,
stream_key: streamData.stream_key,
});
console.log("Stream data:", streamData);
const time = Math.floor(
(Date.now() - new Date(streamData.start_time).getTime()) / 60000 // Convert to minutes
);
if (time < 60) setTimeStarted(`${time}m ago`);
else if (time < 1440)
setTimeStarted(`${Math.floor(time / 60)}h ${time % 60}m ago`);
else
setTimeStarted(
`${Math.floor(time / 1440)}d ${Math.floor((time % 1440) / 60)}h ${
time % 60
}m ago`
);
} else {
// Just need the stream key if not streaming
const response = await fetch(`/api/user/${username}/stream_key`);
const keyData = await response.json();
setStreamData((prev) => ({
...prev,
stream_key: keyData.stream_key,
}));
}
} catch (error) {
console.error("Error checking stream status:", error);
}
};
const fetchCategories = async () => {
try {
const response = await fetch("/api/categories/popular/100");
const data = await response.json();
setCategories(data);
setFilteredCategories(data);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setStreamData((prev) => ({ ...prev, [name]: value })); setStreamData((prev) => ({ ...prev, [name]: value }));
@@ -201,11 +200,11 @@ const StreamDashboardPage: React.FC = () => {
); );
}; };
const handleStartStream = async () => { const handlePublishStream = async () => {
console.log("Starting stream with data:", streamData); console.log("Starting stream with data:", streamData);
const formData = new FormData(); const formData = new FormData();
formData.append("key", streamData.stream_key); formData.append("data", JSON.stringify(streamData));
try { try {
const response = await fetch("/api/publish_stream", { const response = await fetch("/api/publish_stream", {
@@ -228,6 +227,30 @@ const StreamDashboardPage: React.FC = () => {
const handleUpdateStream = async () => { const handleUpdateStream = async () => {
console.log("Updating stream with data:", streamData); console.log("Updating stream with data:", streamData);
const formData = new FormData();
formData.append("key", streamData.stream_key);
formData.append("title", streamData.title);
formData.append("category_name", streamData.category_name);
if (thumbnail) {
formData.append("thumbnail", thumbnail);
}
try {
const response = await fetch("/api/update_stream", {
method: "POST",
body: formData,
});
if (response.ok) {
console.log("Stream updated successfully");
window.location.reload();
} else {
console.error("Failed to update stream");
}
} catch (error) {
console.error("Error updating stream:", error);
}
}; };
const handleEndStream = async () => { const handleEndStream = async () => {
@@ -392,7 +415,7 @@ const StreamDashboardPage: React.FC = () => {
<div className="flex gap-8"> <div className="flex gap-8">
<Button <Button
onClick={ onClick={
isStreaming ? handleUpdateStream : handleStartStream isStreaming ? handleUpdateStream : handlePublishStream
} }
disabled={!isFormValid()} disabled={!isFormValid()}
extraClasses="text-2xl px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed" extraClasses="text-2xl px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed"

View File

@@ -1,11 +1,11 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { ToggleButton } from "../components/Input/Button"; import { ToggleButton } from "../components/Input/Button";
import ChatPanel from "../components/Video/ChatPanel"; import ChatPanel from "../components/Stream/ChatPanel";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useAuthModal } from "../hooks/useAuthModal"; import { useAuthModal } from "../hooks/useAuthModal";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import { useFollow } from "../hooks/useFollow"; import { useFollow } from "../hooks/useFollow";
import VideoPlayer from "../components/Video/VideoPlayer"; import VideoPlayer from "../components/Stream/VideoPlayer";
import { SocketProvider } from "../context/SocketContext"; import { SocketProvider } from "../context/SocketContext";
import AuthModal from "../components/Auth/AuthModal"; import AuthModal from "../components/Auth/AuthModal";
import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm"; import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";

View File

@@ -1,4 +1,5 @@
worker_processes 1; worker_processes 1;
error_log /var/log/nginx/error.log warn;
events { events {
worker_connections 1024; worker_connections 1024;
@@ -15,8 +16,8 @@ rtmp {
deny play all; deny play all;
push rtmp://127.0.0.1:1935/hls-live; push rtmp://127.0.0.1:1935/hls-live;
on_publish http://web_server:5000/publish_stream; on_publish http://web_server:5000/init_stream; # if the stream is detected from OBS
on_publish_done http://web_server:5000/end_stream; on_publish_done http://web_server:5000/end_stream; # if the stream is ended on OBS
} }
@@ -24,7 +25,7 @@ rtmp {
live on; live on;
hls on; hls on;
hls_path /stream_data/; hls_path /stream_data/stream/;
allow publish 127.0.0.1; allow publish 127.0.0.1;
deny publish all; deny publish all;
@@ -78,7 +79,7 @@ http {
# The MPEG-TS video chunks are stored in /tmp/hls # The MPEG-TS video chunks are stored in /tmp/hls
location ~ ^/stream/(.+)/(.+\.ts)$ { location ~ ^/stream/(.+)/(.+\.ts)$ {
alias /stream_data/$1/stream/$2; alias /stream_data/stream/$1/$2;
# Let the MPEG-TS video chunks be cacheable # Let the MPEG-TS video chunks be cacheable
expires max; expires max;
@@ -86,35 +87,36 @@ http {
# The M3U8 playlists location # The M3U8 playlists location
location ~ ^/stream/(.+)/(.+\.m3u8)$ { location ~ ^/stream/(.+)/(.+\.m3u8)$ {
alias /stream_data/$1/stream/$2; alias /stream_data/stream/$1/$2;
# The M3U8 playlists should not be cacheable # The M3U8 playlists should not be cacheable
expires -1d; expires -1d;
} }
#! Unused right now so the following are inaccurate locations
# The thumbnails location # The thumbnails location
location ~ ^/stream/(.+)/thumbnails/(.+\.jpg)$ { # location ~ ^/stream/(.+)/thumbnails/(.+\.jpg)$ {
alias /stream_data/$1/thumbnails/$2; # alias /stream_data/$1/thumbnails/$2;
# The thumbnails should not be cacheable # # The thumbnails should not be cacheable
expires -1d; # expires -1d;
} # }
# The vods location # # The vods location
location ~ ^/stream/(.+)/vods/(.+\.mp4)$ { # location ~ ^/stream/(.+)/vods/(.+\.mp4)$ {
alias /stream_data/$1/vods/$2; # alias /stream_data/$1/vods/$2;
# The vods should not be cacheable # # The vods should not be cacheable
expires -1d; # expires -1d;
} # }
location ~ ^/\?token=.*$ { # location ~ ^/\?token=.*$ {
proxy_pass http://frontend:5173; # proxy_pass http://frontend:5173;
proxy_http_version 1.1; # proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # proxy_set_header Connection "upgrade";
proxy_set_header Host $host; # proxy_set_header Host $host;
} # }
location / { location / {
proxy_pass http://frontend:5173; proxy_pass http://frontend:5173;

View File

@@ -72,7 +72,7 @@ def stream_data(streamer_id) -> dict:
Returns a streamer's current stream data Returns a streamer's current stream data
""" """
data = get_current_stream_data(streamer_id) data = get_current_stream_data(streamer_id)
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(
@@ -163,10 +163,39 @@ def vods(username):
# RTMP Server Routes # RTMP Server Routes
@stream_bp.route("/init_stream", methods=["POST"])
def init_stream():
"""
Called by NGINX when OBS starts streaming.
Creates necessary directories and validates stream key.
"""
stream_key = request.form.get("name")
print(f"Stream initialization requested in nginx with key: {stream_key}")
with Database() as db:
# Check if valid stream key and user is allowed to stream
user_info = db.fetchone("""SELECT user_id, username, is_live
FROM users
WHERE stream_key = ?""", (stream_key,))
if not user_info:
print("Unauthorized - Invalid stream key", flush=True)
return "Unauthorized - Invalid stream key", 403
# Create necessary directories
username = user_info["username"]
create_local_directories(username)
return "OK", 200
@stream_bp.route("/publish_stream", methods=["POST"]) @stream_bp.route("/publish_stream", methods=["POST"])
def publish_stream(): def publish_stream():
""" """
Authenticates stream from streamer and publishes it to the site Called when user clicks Start Stream in dashboard.
Sets up stream in database and starts thumbnail generation.
step-by-step: step-by-step:
fetch user info from stream key fetch user info from stream key
@@ -174,27 +203,26 @@ def publish_stream():
set user as streaming set user as streaming
periodically update thumbnail periodically update thumbnail
""" """
stream_key = request.form.get("key") data = request.form.get("data")
print("Stream request received")
# Open database connection
with Database() as db: with Database() as db:
# Get user info from stream key user_info = db.fetchone("""SELECT user_id, username, current_stream_title,
user_info = db.fetchone("""SELECT user_id, username, current_stream_title, current_selected_category_id, is_live current_selected_category_id, is_live
FROM users FROM users
WHERE stream_key = ?""", (stream_key,)) WHERE stream_key = ?""", (data['stream_key'],))
# If stream key is invalid, return unauthorized
if not user_info or user_info["is_live"]: if not user_info or user_info["is_live"]:
print(
"Unauthorized. No user found from Stream key or user is already streaming.", flush=True)
return "Unauthorized", 403 return "Unauthorized", 403
# Insert stream into database # Insert stream into database
db.execute("""INSERT INTO streams (user_id, title, start_time, num_viewers, category_id) db.execute("""INSERT INTO streams (user_id, title, start_time, num_viewers, category_id)
VALUES (?, ?, ?, ?, ?)""", (user_info["user_id"], VALUES (?, ?, ?, ?, ?)""", (user_info["user_id"],
user_info["current_stream_title"], data["title"],
datetime.now(), datetime.now(),
0, 0,
1)) get_category_id(data['category_name'])))
# Set user as streaming # Set user as streaming
db.execute("""UPDATE users SET is_live = 1 WHERE user_id = ?""", db.execute("""UPDATE users SET is_live = 1 WHERE user_id = ?""",
@@ -203,16 +231,41 @@ def publish_stream():
username = user_info["username"] username = user_info["username"]
user_id = user_info["user_id"] user_id = user_info["user_id"]
# Local file creation
create_local_directories(username)
# Update thumbnail periodically # Update thumbnail periodically
update_thumbnail.delay(user_id, update_thumbnail.delay(user_id,
path_manager.get_stream_file_path(username), path_manager.get_stream_file_path(username),
path_manager.get_thumbnail_file_path(username), path_manager.get_thumbnail_file_path(username),
THUMBNAIL_GENERATION_INTERVAL) THUMBNAIL_GENERATION_INTERVAL)
return redirect(f"/{user_info['username']}/stream/") return "OK", 200
@stream_bp.route("/update_stream", methods=["POST"])
def update_stream():
"""
Called by StreamDashboard to update stream info
"""
# TODO: Add thumbnails (paths) to table, allow user to update thumbnail
stream_key = request.form.get("key")
title = request.form.get("title")
category_name = request.form.get("category_name")
with Database() as db:
user_info = db.fetchone("""SELECT user_id, username, is_live
FROM users
WHERE stream_key = ?""", (stream_key,))
if not user_info or not user_info["is_live"]:
print(
"Unauthorized - No user found from stream key or user is not streaming", flush=True)
return "Unauthorized", 403
db.execute("""UPDATE streams
SET title = ?, category_id = ?
WHERE user_id = ?""", (title, get_category_id(category_name), user_info["user_id"]))
return "Stream updated", 200
@stream_bp.route("/end_stream", methods=["POST"]) @stream_bp.route("/end_stream", methods=["POST"])
@@ -230,6 +283,12 @@ def end_stream():
""" """
stream_key = request.form.get("key") stream_key = request.form.get("key")
if stream_key is None:
stream_key = request.form.get("name")
if stream_key is None:
print("Unauthorized - No stream key provided", flush=True)
return "Unauthorized", 403
# Open database connection # Open database connection
with Database() as db: with Database() as db:
@@ -244,6 +303,7 @@ def end_stream():
# If stream key is invalid, return unauthorized # 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)
return "Unauthorized", 403 return "Unauthorized", 403
# Remove stream from database # Remove stream from database
@@ -256,9 +316,9 @@ def end_stream():
db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views) db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views)
VALUES (?, ?, ?, ?, ?, ?)""", (user_info["user_id"], VALUES (?, ?, ?, ?, ?, ?)""", (user_info["user_id"],
user_info["current_stream_title"], stream_info["title"],
stream_info["start_time"], stream_info["start_time"],
user_info["current_selected_category_id"], stream_info["category_id"],
stream_length, stream_length,
0)) 0))

View File

@@ -142,41 +142,14 @@ def get_vod_tags(vod_id: int):
""", (vod_id,)) """, (vod_id,))
return tags return tags
def transfer_stream_to_vod(user_id: int):
"""
Deletes stream from stream table and moves it to VoD table
TODO: Add functionaliy to save stream permanently
"""
with Database() as db:
stream = db.fetchone("""
SELECT * FROM streams WHERE user_id = ?;
""", (user_id,))
if not stream:
return False
## TODO: calculate length in seconds, currently using temp value
db.execute("""
INSERT INTO vods (user_id, title, datetime, category_id, length, views)
VALUES (?, ?, ?, ?, ?, ?);
""", (stream["user_id"], stream["title"], stream["datetime"], stream["category_id"], 10, stream["num_viewers"]))
db.execute("""
DELETE FROM streams WHERE user_id = ?;
""", (user_id,))
return True
def create_local_directories(username: str): def create_local_directories(username: str):
""" """
Create directories for user stream data if they do not exist Create directories for user stream data if they do not exist
""" """
vods_path = f"stream_data/{username}/vods" vods_path = f"stream_data/vods/{username}"
stream_path = f"stream_data/{username}/stream" stream_path = f"stream_data/stream"
thumbnail_path = f"stream_data/{username}/thumbnails" thumbnail_path = f"stream_data/thumbnails/{username}"
if not os.path.exists(vods_path): if not os.path.exists(vods_path):
os.makedirs(vods_path) os.makedirs(vods_path)
@@ -188,7 +161,6 @@ def create_local_directories(username: str):
os.makedirs(thumbnail_path) os.makedirs(thumbnail_path)
# Fix permissions # Fix permissions
os.chmod(f"stream_data/{username}", 0o777)
os.chmod(vods_path, 0o777) os.chmod(vods_path, 0o777)
os.chmod(stream_path, 0o777) os.chmod(stream_path, 0o777)
os.chmod(thumbnail_path, 0o777) os.chmod(thumbnail_path, 0o777)