FEAT: Add ability to download one's VODs from dashboard
This commit is contained in:
@@ -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}`)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -48,6 +48,80 @@ 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()
|
||||||
|
|
||||||
|
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]:
|
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 +151,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 +159,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 +167,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