FEAT: Add ability to download one's VODs from dashboard

This commit is contained in:
Chris-1010
2025-03-03 01:47:47 +00:00
parent 11c2d67a43
commit c7d443d8b6
6 changed files with 97 additions and 26 deletions

View File

@@ -110,6 +110,7 @@ interface VodListItemProps extends BaseListItemProps, Omit<VodType, "type"> {
}
const VodListItem: React.FC<VodListItemProps> = ({
vod_id,
title,
username,
category_name,
@@ -137,7 +138,7 @@ const VodListItem: React.FC<VodListItemProps> = ({
<div className="p-3">
<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>
<div className="flex justify-between items-center mt-2">
<p className="text-xs text-gray-500">{datetime}</p>
@@ -147,20 +148,21 @@ const VodListItem: React.FC<VodListItemProps> = ({
</div>
{variant === "vodDashboard" && (
<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"
onClick={() => console.log("Publish")}
>
<UploadIcon />
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"
onClick={() => console.log("Download")}
href={`/vods/${username}/${vod_id}.mp4`}
download={`${username}_vod_${vod_id}.mp4`}
>
<DownloadIcon />
Download
</button>
</a>
</div>
)}
</div>

View File

@@ -164,7 +164,6 @@ const ListRow = forwardRef<ListRowRef, ListRowProps>((props, ref) => {
username={item.username}
isLive={item.isLive}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() => onItemClick(item.username)}
extraClasses={itemExtraClasses}
/>

View File

@@ -141,7 +141,7 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses = "" }) => {
return (
<div
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"
onClick={() => (window.location.href = `/category/${category.category_name}`)}
>

View File

@@ -20,11 +20,11 @@ export default defineConfig({
target: "http://localhost:8080",
changeOrigin: true,
ws: true,
},
"/stream": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
"/stream": {
target: "http://localhost:8080",
changeOrigin: true,
},
"/images": {
target: "http://localhost:8080",
changeOrigin: true,
@@ -33,6 +33,10 @@ export default defineConfig({
target: "http://localhost:8080",
changeOrigin: true,
},
"/vods": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
},
build: {

View File

@@ -99,8 +99,8 @@ http {
alias /user_data/$1/vods/$2;
# where $1 is the user's username and $2 is the thumbnail_name
# The thumbnails should not be cacheable
expires -1d;
# The thumbnails should not be cacheable
expires -1d;
}
## Profile pictures location

View File

@@ -48,6 +48,80 @@ def get_current_stream_data(user_id: int) -> Optional[dict]:
""", (user_id,))
return most_recent_stream
def end_user_stream(stream_key, user_id, username):
"""
Utility function to end a user's stream
Parameters:
stream_key: The stream key of the user
user_id: The ID of the user
username: The username of the user
Returns:
bool: True if stream was ended successfully, False otherwise
"""
from flask import current_app
from datetime import datetime
from dateutil import parser
from celery_tasks.streaming import combine_ts_stream
from utils.path_manager import PathManager
path_manager = PathManager()
if not stream_key or not user_id or not username:
current_app.logger.error("Cannot end stream - missing required information")
return False
try:
# Open database connection
with Database() as db:
# Get stream info
stream_info = db.fetchone("""SELECT *
FROM streams
WHERE user_id = ?""", (user_id,))
# If user is not streaming, just return
if not stream_info:
current_app.logger.info(f"User {username} (ID: {user_id}) is not streaming")
return True, "User is not streaming"
# Remove stream from database
db.execute("""DELETE FROM streams
WHERE user_id = ?""", (user_id,))
# Move stream to vod table
stream_length = int(
(datetime.now() - parser.parse(stream_info.get("start_time"))).total_seconds())
db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views)
VALUES (?, ?, ?, ?, ?, ?)""", (user_id,
stream_info.get("title"),
stream_info.get("start_time"),
stream_info.get("category_id"),
stream_length,
0))
vod_id = db.get_last_insert_id()
# Set user as not streaming
db.execute("""UPDATE users
SET is_live = 0
WHERE user_id = ?""", (user_id,))
# Queue task to combine TS files into MP4
combine_ts_stream.delay(
path_manager.get_stream_path(username),
path_manager.get_vods_path(username),
vod_id
)
current_app.logger.info(f"Stream ended for user {username} (ID: {user_id})")
return True, "Stream ended successfully"
except Exception as e:
current_app.logger.error(f"Error ending stream for user {username}: {str(e)}")
return False, f"Error ending stream: {str(e)}"
def get_category_id(category_name: str) -> Optional[int]:
"""
Returns the category_id given a category name
@@ -77,7 +151,7 @@ def get_vod(vod_id: int) -> dict:
Returns data of a streamers vod
"""
with Database() as db:
vod = db.fetchone("""SELECT * FROM vods WHERE vod_id = ?;""", (vod_id,))
vod = db.fetchone("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vod_id = ?;""", (vod_id,))
return vod
def get_latest_vod(user_id: int):
@@ -85,7 +159,7 @@ def get_latest_vod(user_id: int):
Returns data of the most recent stream by a streamer
"""
with Database() as db:
latest_vod = db.fetchone("""SELECT vods.*, category_name FROM vods JOIN categories ON vods.category_id = categories.category_id WHERE user_id = ? ORDER BY vod_id DESC;""", (user_id,))
latest_vod = db.fetchone("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vods.user_id = ? ORDER BY vod_id DESC;""", (user_id,))
return latest_vod
def get_user_vods(user_id: int):
@@ -93,15 +167,7 @@ def get_user_vods(user_id: int):
Returns data of all vods by a streamer
"""
with Database() as db:
vods = db.fetchall("""SELECT vods.*, category_name FROM vods JOIN categories ON vods.category_id = categories.category_id WHERE user_id = ? ORDER BY vod_id DESC;""", (user_id,))
return vods
def get_all_vods():
"""
Returns data of all VODs by all streamers in a JSON-compatible format
"""
with Database() as db:
vods = db.fetchall("""SELECT * FROM vods""")
vods = db.fetchall("""SELECT vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id WHERE vods.user_id = ? ORDER BY vod_id DESC;""", (user_id,))
return vods
def generate_thumbnail(stream_file: str, thumbnail_file: str) -> None: