Fix/pylint cleanup (#8)
Some checks are pending
CI / build (3.10) (push) Waiting to run
CI / build (3.8) (push) Waiting to run
CI / build (3.9) (push) Waiting to run

* Fix pylint warnings across all 24 Python files in web_server

- Add module, class, and function docstrings (C0114, C0115, C0116)
- Fix import ordering: stdlib before third-party before local (C0411)
- Replace wildcard imports with explicit named imports (W0401)
- Remove trailing whitespace and add missing final newlines (C0303, C0304)
- Replace dict() with dict literals (R1735)
- Remove unused imports and variables (W0611, W0612)
- Narrow broad Exception catches to specific exceptions (W0718)
- Replace f-string logging with lazy % formatting (W1203)
- Fix variable naming: UPPER_CASE for constants, snake_case for locals (C0103)
- Add pylint disable comments for necessary global statements (W0603)
- Fix no-else-return, simplifiable-if-expression, singleton-comparison
- Fix bad indentation in stripe.py (W0311)
- Add encoding="utf-8" to open() calls (W1514)
- Add check=True to subprocess.run() calls (W1510)
- Register Celery task modules via conf.include

* Update `package-lock.json` add peer dependencies
This commit is contained in:
Christopher Ahern
2026-02-07 20:57:28 +00:00
committed by GitHub
parent fed1a2f288
commit 2758be8680
25 changed files with 680 additions and 419 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.5.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend", "name": "frontend",
"version": "0.5.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@stripe/react-stripe-js": "^3.1.1", "@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.5.0", "@stripe/stripe-js": "^5.5.0",
@@ -97,6 +97,7 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0", "@babel/code-frame": "^7.26.0",
@@ -1393,6 +1394,7 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.5.0.tgz", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.5.0.tgz",
"integrity": "sha512-lkfjyAd34aeMpTKKcEVfy8IUyEsjuAT3t9EXr5yZDtdIUncnZpedl/xLV16Dkd4z+fQwixScsCCDxSMNtBOgpQ==", "integrity": "sha512-lkfjyAd34aeMpTKKcEVfy8IUyEsjuAT3t9EXr5yZDtdIUncnZpedl/xLV16Dkd4z+fQwixScsCCDxSMNtBOgpQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12.16" "node": ">=12.16"
} }
@@ -1482,6 +1484,7 @@
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -1539,6 +1542,7 @@
"integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.20.0", "@typescript-eslint/scope-manager": "8.20.0",
"@typescript-eslint/types": "8.20.0", "@typescript-eslint/types": "8.20.0",
@@ -1805,6 +1809,7 @@
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2018,6 +2023,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.73",
@@ -2051,9 +2057,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001695", "version": "1.0.30001769",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
"integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2386,6 +2392,7 @@
"integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3534,6 +3541,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.8", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -3723,6 +3731,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -3741,6 +3750,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -4233,6 +4243,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -4351,6 +4362,7 @@
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -4443,6 +4455,7 @@
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.21.0.tgz", "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.21.0.tgz",
"integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==", "integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "^3.16.2", "@videojs/http-streaming": "^3.16.2",
@@ -4495,6 +4508,7 @@
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"postcss": "^8.4.49", "postcss": "^8.4.49",

View File

@@ -1,3 +1,7 @@
"""Flask application factory and blueprint registration."""
from os import getenv
from flask import Flask from flask import Flask
from flask_session import Session from flask_session import Session
from flask_cors import CORS from flask_cors import CORS
@@ -13,10 +17,8 @@ from blueprints.oauth import oauth_bp, init_oauth
from blueprints.socket import socketio from blueprints.socket import socketio
from blueprints.search_bar import search_bp from blueprints.search_bar import search_bp
from celery import Celery
from celery_tasks import celery_init_app from celery_tasks import celery_init_app
from os import getenv
def create_app(): def create_app():
""" """
@@ -34,16 +36,15 @@ def create_app():
app.config['GOOGLE_CLIENT_SECRET'] = getenv("GOOGLE_CLIENT_SECRET") app.config['GOOGLE_CLIENT_SECRET'] = getenv("GOOGLE_CLIENT_SECRET")
app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config.from_mapping( app.config.from_mapping(
CELERY=dict( CELERY={
broker_url="redis://redis:6379/0", "broker_url": "redis://redis:6379/0",
result_backend="redis://redis:6379/0", "result_backend": "redis://redis:6379/0",
task_ignore_result=True, "task_ignore_result": True,
), },
) )
app.config.from_prefixed_env() app.config.from_prefixed_env()
celery = celery_init_app(app) celery_init_app(app)
#! ↓↓↓ For development purposes only - Allow cross-origin requests for the frontend #! ↓↓↓ For development purposes only - Allow cross-origin requests for the frontend
CORS(app, supports_credentials=True) CORS(app, supports_credentials=True)

View File

@@ -1,6 +1,8 @@
"""Admin blueprint for user management operations."""
from flask import Blueprint, session from flask import Blueprint, session
from utils.utils import sanitize from utils.utils import sanitize
from utils.admin_utils import * from utils.admin_utils import check_if_admin, check_if_user_exists, ban_user
admin_bp = Blueprint("admin", __name__) admin_bp = Blueprint("admin", __name__)

View File

@@ -1,3 +1,8 @@
"""Authentication blueprint for user signup, login, and logout."""
import logging
from secrets import token_hex
from flask import Blueprint, session, request, jsonify from flask import Blueprint, session, request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from flask_cors import cross_origin from flask_cors import cross_origin
@@ -5,18 +10,21 @@ from database.database import Database
from blueprints.middleware import login_required from blueprints.middleware import login_required
from utils.user_utils import get_user_id from utils.user_utils import get_user_id
from utils.utils import sanitize from utils.utils import sanitize
from secrets import token_hex
from utils.path_manager import PathManager from utils.path_manager import PathManager
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)
path_manager = PathManager() path_manager = PathManager()
logger = logging.getLogger(__name__)
@auth_bp.route("/signup", methods=["POST"]) @auth_bp.route("/signup", methods=["POST"])
@cross_origin(supports_credentials=True) @cross_origin(supports_credentials=True)
def signup(): def signup():
""" """
Route that allows a user to sign up by providing a `username`, `email` and `password`. Route that allows a user to sign up by providing a
`username`, `email` and `password`.
""" """
# ensure a JSON request is made to contact this route # ensure a JSON request is made to contact this route
if not request.is_json: if not request.is_json:
@@ -30,7 +38,9 @@ def signup():
# Validation - ensure all fields exist, users cannot have an empty field # Validation - ensure all fields exist, users cannot have an empty field
if not all([username, email, password]): if not all([username, email, password]):
error_fields = get_error_fields([username, email, password]) #!←← find the error_fields, to highlight them in red to the user on the frontend error_fields = get_error_fields(
[username, email, password]
)
return jsonify({ return jsonify({
"account_created": False, "account_created": False,
"error_fields": error_fields, "error_fields": error_fields,
@@ -42,7 +52,7 @@ def signup():
username = sanitize(username, "username") username = sanitize(username, "username")
email = sanitize(email, "email") email = sanitize(email, "email")
password = sanitize(password, "password") password = sanitize(password, "password")
except ValueError as e: except ValueError:
error_fields = get_error_fields([username, email, password]) error_fields = get_error_fields([username, email, password])
return jsonify({ return jsonify({
"account_created": False, "account_created": False,
@@ -100,11 +110,11 @@ def signup():
"message": "Account created successfully" "message": "Account created successfully"
}), 201 }), 201
except Exception as e: except (ValueError, TypeError, KeyError) as exc:
print(f"Error during signup: {e}") # Log the error logger.error("Error during signup: %s", exc)
return jsonify({ return jsonify({
"account_created": False, "account_created": False,
"message": "Server error occurred: " + str(e) "message": "Server error occurred: " + str(exc)
}), 500 }), 500
finally: finally:
@@ -127,7 +137,7 @@ def login():
username = data.get('username') username = data.get('username')
password = data.get('password') password = data.get('password')
# Validation - ensure all fields exist, users cannot have an empty field # Validation - ensure all fields exist, users cannot have an empty field
if not all([username, password]): if not all([username, password]):
return jsonify({ return jsonify({
"logged_in": False, "logged_in": False,
@@ -138,7 +148,7 @@ def login():
try: try:
username = sanitize(username, "username") username = sanitize(username, "username")
password = sanitize(password, "password") password = sanitize(password, "password")
except ValueError as e: except ValueError:
return jsonify({ return jsonify({
"account_created": False, "account_created": False,
"error_fields": ["username", "password"], "error_fields": ["username", "password"],
@@ -177,7 +187,10 @@ def login():
session.clear() session.clear()
session["username"] = username session["username"] = username
session["user_id"] = get_user_id(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) logger.info(
"Logged in as %s. session: %s. user_id: %s",
username, session.get('username'), session.get('user_id')
)
# User has been logged in, let frontend know that # User has been logged in, let frontend know that
return jsonify({ return jsonify({
@@ -186,8 +199,8 @@ def login():
"username": username "username": username
}), 200 }), 200
except Exception as e: except (ValueError, TypeError, KeyError) as exc:
print(f"Error during login: {e}") # Log the error logger.error("Error during login: %s", exc)
return jsonify({ return jsonify({
"logged_in": False, "logged_in": False,
"message": "Server error occurred" "message": "Server error occurred"
@@ -206,7 +219,6 @@ def logout() -> dict:
If the user is currently streaming, end their stream first. 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 from utils.stream_utils import end_user_stream
# Check if user is currently streaming # Check if user is currently streaming
@@ -214,12 +226,21 @@ def logout() -> dict:
username = session.get("username") username = session.get("username")
with Database() as db: with Database() as db:
is_streaming = db.fetchone("""SELECT is_live FROM users WHERE user_id = ?""", (user_id,)) is_streaming = db.fetchone(
"""SELECT is_live FROM users WHERE user_id = ?""",
(user_id,)
)
if is_streaming and is_streaming.get("is_live") == 1: if is_streaming and is_streaming.get("is_live") == 1:
# Get the user's stream key # Get the user's stream key
stream_key_info = db.fetchone("""SELECT stream_key FROM users WHERE user_id = ?""", (user_id,)) stream_key_info = db.fetchone(
stream_key = stream_key_info.get("stream_key") if stream_key_info else None """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: if stream_key:
# End the stream # End the stream
@@ -227,6 +248,8 @@ def logout() -> dict:
session.clear() session.clear()
return {"logged_in": False} return {"logged_in": False}
def get_error_fields(values: list): def get_error_fields(values: list):
"""Return field names for empty values."""
fields = ["username", "email", "password"] fields = ["username", "email", "password"]
return [fields[i] for i, v in enumerate(values) if not v] return [fields[i] for i, v in enumerate(values) if not v]

View File

@@ -1,14 +1,17 @@
"""Chat blueprint for WebSocket-based real-time messaging."""
import json
from datetime import datetime
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from database.database import Database from database.database import Database
from .socket import socketio from .socket import socketio
from flask_socketio import emit, join_room, leave_room from flask_socketio import emit, join_room, leave_room
from datetime import datetime from utils.user_utils import is_subscribed
from utils.user_utils import get_user_id, is_subscribed
import redis import redis
import json
redis_url = "redis://redis:6379/1" REDIS_URL = "redis://redis:6379/1"
r = redis.from_url(redis_url, decode_responses=True) r = redis.from_url(REDIS_URL, decode_responses=True)
chat_bp = Blueprint("chat", __name__) chat_bp = Blueprint("chat", __name__)
@socketio.on("connect") @socketio.on("connect")
@@ -125,7 +128,13 @@ def send_chat(data) -> None:
# Input validation - chatter is logged in, message is not empty, stream exists # Input validation - chatter is logged in, message is not empty, stream exists
if not all([chatter_name, message, stream_id]): if not all([chatter_name, message, stream_id]):
emit("error", {"error": f"Unable to send a chat. The following info was given: chatter_name={chatter_name}, message={message}, stream_id={stream_id}"}, broadcast=False) emit("error", {
"error": (
f"Unable to send a chat. The following info was given: "
f"chatter_name={chatter_name}, message={message}, "
f"stream_id={stream_id}"
)
}, broadcast=False)
return return
subscribed = is_subscribed(chatter_id, stream_id) subscribed = is_subscribed(chatter_id, stream_id)
# Send the chat message to the client so it can be displayed # Send the chat message to the client so it can be displayed
@@ -161,9 +170,8 @@ def update_viewers(user_id, num_viewers):
SET num_viewers = ? SET num_viewers = ?
WHERE user_id = ?; WHERE user_id = ?;
""", (num_viewers, user_id)) """, (num_viewers, user_id))
db.close_connection db.close_connection()
#TODO: Make sure that users entry within Redis is removed if they disconnect from socket
def add_favourability_entry(user_id, stream_id): def add_favourability_entry(user_id, stream_id):
""" """
Adds entry to Redis that user is watching a streamer Adds entry to Redis that user is watching a streamer

View File

@@ -1,7 +1,10 @@
from flask import redirect, g, session """Authentication middleware and error handler registration."""
from functools import wraps
import logging import logging
from functools import wraps
from os import getenv from os import getenv
from flask import redirect, g, session
from dotenv import load_dotenv from dotenv import load_dotenv
from database.database import Database from database.database import Database
@@ -57,5 +60,5 @@ def register_error_handlers(app):
for code, message in error_responses.items(): for code, message in error_responses.items():
@app.errorhandler(code) @app.errorhandler(code)
def handle_error(error, message=message, code=code): def handle_error(error, message=message, code=code):
logging.error(f"Error {code}: {str(error)}") logging.error("Error %d: %s", code, str(error))
return {"error": message}, code return {"error": message}, code

View File

@@ -1,15 +1,18 @@
"""OAuth blueprint for Google authentication."""
from os import getenv from os import getenv
from secrets import token_hex, token_urlsafe
from random import randint
from authlib.integrations.flask_client import OAuth, OAuthError from authlib.integrations.flask_client import OAuth, OAuthError
from flask import Blueprint, jsonify, session, redirect, request from flask import Blueprint, jsonify, session, redirect, request
from blueprints.user import get_session_info_email from blueprints.user import get_session_info_email
from database.database import Database from database.database import Database
from dotenv import load_dotenv from dotenv import load_dotenv
from secrets import token_hex, token_urlsafe
from random import randint
from utils.path_manager import PathManager from utils.path_manager import PathManager
oauth_bp = Blueprint("oauth", __name__) oauth_bp = Blueprint("oauth", __name__)
google = None _google = None
load_dotenv() load_dotenv()
url_api = getenv("VITE_API_URL") url_api = getenv("VITE_API_URL")
@@ -23,8 +26,8 @@ def init_oauth(app):
Initialise the OAuth functionality. Initialise the OAuth functionality.
""" """
oauth = OAuth(app) oauth = OAuth(app)
global google global _google # pylint: disable=global-statement
google = oauth.register( _google = oauth.register(
'google', 'google',
client_id=app.config['GOOGLE_CLIENT_ID'], client_id=app.config['GOOGLE_CLIENT_ID'],
client_secret=app.config['GOOGLE_CLIENT_SECRET'], client_secret=app.config['GOOGLE_CLIENT_SECRET'],
@@ -54,7 +57,7 @@ def login_google():
# Make sure session is saved before redirect # Make sure session is saved before redirect
session.modified = True session.modified = True
return google.authorize_redirect( return _google.authorize_redirect(
redirect_uri=f'{url}/api/google_auth', redirect_uri=f'{url}/api/google_auth',
nonce=session['nonce'], nonce=session['nonce'],
state=session['state'] state=session['state']
@@ -72,21 +75,25 @@ def google_auth():
stored_state = session.get('state') stored_state = session.get('state')
if not stored_state or stored_state != returned_state: if not stored_state or stored_state != returned_state:
print(f"State mismatch: stored={stored_state}, returned={returned_state}", flush=True) print(
f"State mismatch: stored={stored_state}, "
f"returned={returned_state}", flush=True
)
return jsonify({ return jsonify({
'error': f"mismatching_state: CSRF Warning! State not equal in request and response.", 'error': "mismatching_state: CSRF Warning! "
"State not equal in request and response.",
'message': 'Authentication failed' 'message': 'Authentication failed'
}), 400 }), 400
# State matched, proceed with token authorization # State matched, proceed with token authorization
token = google.authorize_access_token() token = _google.authorize_access_token()
# Verify nonce # Verify nonce
nonce = session.get('nonce') nonce = session.get('nonce')
if not nonce: if not nonce:
return jsonify({'error': 'Missing nonce in session'}), 400 return jsonify({'error': 'Missing nonce in session'}), 400
user = google.parse_id_token(token, nonce=nonce) user = _google.parse_id_token(token, nonce=nonce)
# Check if email exists to login else create a database entry # Check if email exists to login else create a database entry
user_email = user.get("email") user_email = user.get("email")
@@ -133,7 +140,10 @@ def google_auth():
# Ensure session is saved # Ensure session is saved
session.modified = True session.modified = True
print(f"session: {session.get('username')}. user_id: {session.get('user_id')}", flush=True) print(
f"session: {session.get('username')}. "
f"user_id: {session.get('user_id')}", flush=True
)
return redirect(origin) return redirect(origin)
@@ -144,7 +154,7 @@ def google_auth():
'error': str(e) 'error': str(e)
}), 400 }), 400
except Exception as e: except (ValueError, TypeError, KeyError) as e:
print(f"Unexpected Error: {str(e)}", flush=True) print(f"Unexpected Error: {str(e)}", flush=True)
return jsonify({ return jsonify({
'message': 'An unexpected error occurred', 'message': 'An unexpected error occurred',

View File

@@ -1,3 +1,5 @@
"""Search bar blueprint for querying categories, users, streams, and VODs."""
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from database.database import Database from database.database import Database
from utils.utils import sanitize from utils.utils import sanitize
@@ -20,7 +22,7 @@ def rank_results(query, result):
# Assign a score based on the level of the match # Assign a score based on the level of the match
if query in result: if query in result:
return 0 return 0
elif all(c in charset for c in query): if all(c in charset for c in query):
return 1 return 1
return 2 return 2

View File

@@ -1,3 +1,5 @@
"""WebSocket configuration using Flask-SocketIO."""
from flask_socketio import SocketIO from flask_socketio import SocketIO
socketio = SocketIO( socketio = SocketIO(

View File

@@ -1,15 +1,24 @@
"""Stream and VOD management blueprint."""
import json
from datetime import datetime
from flask import Blueprint, session, jsonify, request, redirect from flask import Blueprint, session, jsonify, request, redirect
from utils.stream_utils import * from utils.stream_utils import (
from utils.recommendation_utils import * get_category_id, get_current_stream_data, get_streamer_live_status,
get_latest_vod, get_vod, get_user_vods, end_user_stream
)
from utils.recommendation_utils import (
get_user_preferred_category, get_highest_view_streams,
get_streams_based_on_category, get_highest_view_categories,
get_user_category_recommendations, get_followed_categories_recommendations
)
from utils.user_utils import get_user_id from utils.user_utils import get_user_id
from blueprints.middleware import login_required from blueprints.middleware import login_required
from database.database import Database from database.database import Database
from datetime import datetime from celery_tasks.streaming import update_thumbnail
from celery_tasks.streaming import update_thumbnail, combine_ts_stream
from dateutil import parser
from utils.path_manager import PathManager from utils.path_manager import PathManager
from PIL import Image from PIL import Image
import json
stream_bp = Blueprint("stream", __name__) stream_bp = Blueprint("stream", __name__)
@@ -28,12 +37,12 @@ def popular_streams(no_streams) -> list[dict]:
Returns a list of streams live now with the highest viewers Returns a list of streams live now with the highest viewers
""" """
# Limit the number of streams to MAX_STREAMS # Limit the number of streams to max_streams
MAX_STREAMS = 100 max_streams = 100
if no_streams < 1: if no_streams < 1:
return jsonify([]) return jsonify([])
elif no_streams > MAX_STREAMS: if no_streams > max_streams:
no_streams = MAX_STREAMS no_streams = max_streams
# Get the highest viewed streams # Get the highest viewed streams
streams = get_highest_view_streams(no_streams) streams = get_highest_view_streams(no_streams)
@@ -101,7 +110,7 @@ def popular_categories(no_categories=4, offset=0) -> list[dict]:
# Limit the number of categories to 100 # Limit the number of categories to 100
if no_categories < 1: if no_categories < 1:
return jsonify([]) return jsonify([])
elif no_categories > 100: if no_categories > 100:
no_categories = 100 no_categories = 100
category_data = get_highest_view_categories(no_categories, offset) category_data = get_highest_view_categories(no_categories, offset)
@@ -135,7 +144,8 @@ def following_categories_streams():
@stream_bp.route('/user/<string:username>/status') @stream_bp.route('/user/<string:username>/status')
def user_live_status(username): def user_live_status(username):
""" """
Returns a streamer's status, if they are live or not and their most recent stream (as a vod) (their current stream if live) Returns a streamer's status, if they are live or not and their
most recent stream (as a vod) (their current stream if live)
Returns: Returns:
{ {
"is_live": bool, "is_live": bool,
@@ -146,7 +156,7 @@ def user_live_status(username):
user_id = get_user_id(username) user_id = get_user_id(username)
# Check if streamer is live and get their most recent vod # Check if streamer is live and get their most recent vod
is_live = True if get_streamer_live_status(user_id)['is_live'] else False is_live = bool(get_streamer_live_status(user_id)['is_live'])
most_recent_vod = get_latest_vod(user_id) most_recent_vod = get_latest_vod(user_id)
# If there is no most recent vod, set it to None # If there is no most recent vod, set it to None
@@ -171,25 +181,24 @@ def user_live_status_direct(username):
""" """
user_id = get_user_id(username) user_id = get_user_id(username)
is_live = True if get_streamer_live_status(user_id)['is_live'] else False is_live = bool(get_streamer_live_status(user_id)['is_live'])
if is_live: if is_live:
return 'ok', 200 return 'ok', 200
else: return 'not live', 404
return 'not live', 404
# VOD Routes # VOD Routes
@stream_bp.route('/vods/<int:vod_id>') @stream_bp.route('/vods/<int:vod_id>')
def vod(vod_id): def single_vod(vod_id):
""" """
Returns details about a specific vod Returns details about a specific vod
""" """
vod = get_vod(vod_id) vod_data = get_vod(vod_id)
return jsonify(vod) return jsonify(vod_data)
@stream_bp.route('/vods/<string:username>') @stream_bp.route('/vods/<string:username>')
def vods(username): def user_vods(username):
""" """
Returns a JSON of all the vods of a streamer Returns a JSON of all the vods of a streamer
Returns: Returns:
@@ -207,8 +216,8 @@ def vods(username):
""" """
user_id = get_user_id(username) user_id = get_user_id(username)
vods = get_user_vods(user_id) vod_list = get_user_vods(user_id)
return jsonify(vods) return jsonify(vod_list)
@stream_bp.route('/vods/all') @stream_bp.route('/vods/all')
def get_all_vods(): def get_all_vods():
@@ -216,9 +225,14 @@ 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 vods.*, username, category_name FROM vods JOIN users ON vods.user_id = users.user_id JOIN categories ON vods.category_id = categories.category_id;""") all_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;"""
)
return jsonify(vods) return jsonify(all_vods)
# RTMP Server Routes # RTMP Server Routes
@@ -232,9 +246,10 @@ def init_stream():
with Database() as db: with Database() as db:
# Check if valid stream key and user is allowed to stream # Check if valid stream key and user is allowed to stream
user_info = db.fetchone("""SELECT user_id, username, is_live user_info = db.fetchone(
FROM users """SELECT user_id, username, is_live
WHERE stream_key = ?""", (stream_key,)) FROM users
WHERE stream_key = ?""", (stream_key,))
# No user found from stream key # No user found from stream key
if not user_info: if not user_info:
@@ -280,25 +295,27 @@ def publish_stream():
username = None username = None
with Database() as db: with Database() as db:
user_info = db.fetchone("""SELECT user_id, username, is_live user_info = db.fetchone(
FROM users """SELECT user_id, username, is_live
WHERE stream_key = ?""", (stream_key,)) FROM users
WHERE stream_key = ?""", (stream_key,))
if not user_info or user_info.get("is_live"): if not user_info or user_info.get("is_live"):
print( print(
"Unauthorized. No user found from Stream key or user is already streaming.", flush=True) "Unauthorized. No user found from Stream key "
"or user is already streaming.", flush=True)
return "Unauthorized", 403 return "Unauthorized", 403
user_id = user_info.get("user_id") user_id = user_info.get("user_id")
username = user_info.get("username") username = user_info.get("username")
# Insert stream into database # Insert stream into database
db.execute("""INSERT INTO streams (user_id, title, start_time, num_viewers, category_id) db.execute(
VALUES (?, ?, ?, ?, ?)""", (user_id, """INSERT INTO streams
stream_title, (user_id, title, start_time, num_viewers, category_id)
datetime.now(), VALUES (?, ?, ?, ?, ?)""",
0, (user_id, stream_title, datetime.now(), 0,
get_category_id(stream_category))) get_category_id(stream_category)))
# 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 = ?""",
@@ -306,10 +323,12 @@ def publish_stream():
# Update thumbnail periodically only if a custom thumbnail is not provided # Update thumbnail periodically only if a custom thumbnail is not provided
if not stream_thumbnail: if not stream_thumbnail:
update_thumbnail.apply_async((user_id, update_thumbnail.apply_async(
path_manager.get_stream_file_path(username), (user_id,
path_manager.get_current_stream_thumbnail_file_path(username), path_manager.get_stream_file_path(username),
THUMBNAIL_GENERATION_INTERVAL), countdown=10) path_manager.get_current_stream_thumbnail_file_path(username),
THUMBNAIL_GENERATION_INTERVAL),
countdown=10)
return "OK", 200 return "OK", 200
@@ -332,30 +351,35 @@ def update_stream():
with Database() as db: with Database() as db:
user_info = db.fetchone("""SELECT user_id, username, is_live user_info = db.fetchone(
FROM users """SELECT user_id, username, is_live
WHERE stream_key = ?""", (stream_key,)) FROM users
WHERE stream_key = ?""", (stream_key,))
if not user_info or not user_info.get("is_live"): if not user_info or not user_info.get("is_live"):
print( print(
"Unauthorized - No user found from stream key or user is not streaming", flush=True) "Unauthorized - No user found from stream key "
"or user is not streaming", flush=True)
return "Unauthorized", 403 return "Unauthorized", 403
user_id = user_info.get("user_id") user_id = user_info.get("user_id")
username = user_info.get("username") username = user_info.get("username")
# TODO: Add update to thumbnail here # TODO: Add update to thumbnail here
db.execute("""UPDATE streams db.execute(
SET title = ?, category_id = ? """UPDATE streams
WHERE user_id = ?""", (stream_title, get_category_id(stream_category), user_id)) SET title = ?, category_id = ?
WHERE user_id = ?""",
(stream_title, get_category_id(stream_category), user_id))
print("GOT: " + stream_thumbnail, flush=True) print("GOT: " + stream_thumbnail, flush=True)
if stream_thumbnail: if stream_thumbnail:
# Set custom thumbnail status to true # Set custom thumbnail status to true
db.execute("""UPDATE streams db.execute(
SET custom_thumbnail = ? """UPDATE streams
WHERE user_id = ?""", (True, user_id)) SET custom_thumbnail = ?
WHERE user_id = ?""", (True, user_id))
# Get thumbnail path # Get thumbnail path
thumbnail_path = path_manager.get_current_stream_thumbnail_file_path(username) thumbnail_path = path_manager.get_current_stream_thumbnail_file_path(username)
@@ -390,9 +414,10 @@ def end_stream():
# Get user info from stream key # Get user info from stream key
with Database() as db: with Database() as db:
user_info = db.fetchone("""SELECT user_id, username user_info = db.fetchone(
FROM users """SELECT user_id, username
WHERE stream_key = ?""", (stream_key,)) FROM users
WHERE stream_key = ?""", (stream_key,))
# Return unauthorized if no user found # Return unauthorized if no user found
if not user_info: if not user_info:

View File

@@ -1,7 +1,11 @@
"""Stripe payment integration blueprint."""
import os
from flask import Blueprint, request, jsonify, session as s from flask import Blueprint, request, jsonify, session as s
from blueprints.middleware import login_required from blueprints.middleware import login_required
from utils.user_utils import subscribe from utils.user_utils import subscribe
import os, stripe import stripe
stripe_bp = Blueprint("stripe", __name__) stripe_bp = Blueprint("stripe", __name__)
@@ -20,7 +24,7 @@ def create_checkout_session():
# Checks to see who is subscribing to who # Checks to see who is subscribing to who
user_id = s.get("user_id") user_id = s.get("user_id")
streamer_id = request.args.get("streamer_id") streamer_id = request.args.get("streamer_id")
session = stripe.checkout.Session.create( checkout_session = stripe.checkout.Session.create(
ui_mode = 'embedded', ui_mode = 'embedded',
payment_method_types=['card'], payment_method_types=['card'],
line_items=[ line_items=[
@@ -33,19 +37,24 @@ def create_checkout_session():
redirect_on_completion = 'never', redirect_on_completion = 'never',
client_reference_id = f"{user_id}-{streamer_id}" client_reference_id = f"{user_id}-{streamer_id}"
) )
except Exception as e: except (ValueError, TypeError, KeyError) as e:
return str(e), 500 return str(e), 500
return jsonify(clientSecret=session.client_secret) return jsonify(clientSecret=checkout_session.client_secret)
@stripe_bp.route('/session-status') # check for payment status @stripe_bp.route('/session-status') # check for payment status
def session_status(): def session_status():
""" """
Used to query payment status Used to query payment status
""" """
session = stripe.checkout.Session.retrieve(request.args.get('session_id')) checkout_session = stripe.checkout.Session.retrieve(
request.args.get('session_id')
)
return jsonify(status=session.status, customer_email=session.customer_details.email) return jsonify(
status=checkout_session.status,
customer_email=checkout_session.customer_details.email
)
@stripe_bp.route('/stripe/webhook', methods=['POST']) @stripe_bp.route('/stripe/webhook', methods=['POST'])
def stripe_webhook(): def stripe_webhook():
@@ -66,12 +75,19 @@ def stripe_webhook():
except stripe.error.SignatureVerificationError as e: except stripe.error.SignatureVerificationError as e:
raise e raise e
if event['type'] == "checkout.session.completed": # Handles payment success webhook # Handles payment success webhook
session = event['data']['object'] if event['type'] == "checkout.session.completed":
product_id = stripe.checkout.Session.list_line_items(session['id'])['data'][0]['price']['product'] checkout_session = event['data']['object']
product_id = stripe.checkout.Session.list_line_items(
checkout_session['id']
)['data'][0]['price']['product']
if product_id == subscription: if product_id == subscription:
client_reference_id = session.get("client_reference_id") client_reference_id = checkout_session.get(
user_id, streamer_id = map(int, client_reference_id.split("-")) "client_reference_id"
)
user_id, streamer_id = map(
int, client_reference_id.split("-")
)
subscribe(user_id, streamer_id) subscribe(user_id, streamer_id)
return "Success", 200 return "Success", 200

View File

@@ -1,17 +1,28 @@
"""User profile and account management blueprint."""
from flask import Blueprint, jsonify, session, request from flask import Blueprint, jsonify, session, request
from utils.user_utils import * from utils.user_utils import (
from utils.auth import * get_user_id, get_user, update_bio, is_subscribed,
subscription_expiration, delete_subscription, is_following,
follow, unfollow, get_followed_streamers, get_followed_categories,
is_following_category, follow_category, unfollow_category,
has_password, get_session_info_email
)
from database.database import Database
from utils.auth import verify_token, reset_password
from utils.utils import get_category_id from utils.utils import get_category_id
from blueprints.middleware import login_required from blueprints.middleware import login_required
from utils.email import send_email, forgot_password_body, newsletter_conf, remove_from_newsletter, email_exists from utils.email import (
send_email, forgot_password_body, newsletter_conf,
remove_from_newsletter, email_exists
)
from utils.path_manager import PathManager from utils.path_manager import PathManager
from celery_tasks.streaming import convert_image_to_png
import redis import redis
from PIL import Image from PIL import Image
redis_url = "redis://redis:6379/1" REDIS_URL = "redis://redis:6379/1"
r = redis.from_url(redis_url, decode_responses=True) r = redis.from_url(REDIS_URL, decode_responses=True)
user_bp = Blueprint("user", __name__) user_bp = Blueprint("user", __name__)
@@ -24,7 +35,7 @@ def user_data(username: str):
""" """
user_id = get_user_id(username) user_id = get_user_id(username)
if not user_id: if not user_id:
jsonify({"error": "User not found from username"}), 404 return jsonify({"error": "User not found from username"}), 404
data = get_user(user_id) data = get_user(user_id)
return jsonify(data) return jsonify(data)
@@ -73,7 +84,7 @@ def user_change_bio():
bio = data.get("bio") bio = data.get("bio")
update_bio(user_id, bio) update_bio(user_id, bio)
return jsonify({"status": "Success"}), 200 return jsonify({"status": "Success"}), 200
except Exception as e: except (ValueError, TypeError, KeyError) as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
## Subscription Routes ## Subscription Routes
@@ -198,13 +209,14 @@ def user_forgot_password(email):
exists = email_exists(email) exists = email_exists(email)
password = has_password(email) password = has_password(email)
# Checks if password exists and is not a Google OAuth account # Checks if password exists and is not a Google OAuth account
if(exists and password): if exists and password:
send_email(email, lambda: forgot_password_body(email)) send_email(email, lambda: forgot_password_body(email))
return email return email
return jsonify({"error":"Invalid email or not found"}), 404 return jsonify({"error": "Invalid email or not found"}), 404
@user_bp.route("/send_newsletter/<string:email>", methods=["POST"]) @user_bp.route("/send_newsletter/<string:email>", methods=["POST"])
def send_newsletter(email): def send_newsletter(email):
"""Sends a newsletter confirmation email."""
send_email(email, lambda: newsletter_conf(email)) send_email(email, lambda: newsletter_conf(email))
return email return email
@@ -226,6 +238,7 @@ def user_reset_password(token, new_password):
@user_bp.route("/user/unsubscribe/<string:token>", methods=["POST"]) @user_bp.route("/user/unsubscribe/<string:token>", methods=["POST"])
def unsubscribe(token): def unsubscribe(token):
"""Unsubscribes a user from the newsletter."""
salt = r.get(token) salt = r.get(token)
if salt: if salt:
r.delete(token) r.delete(token)

View File

@@ -1,7 +1,12 @@
from celery import Celery, shared_task, Task """Celery configuration and Flask app context setup for async tasks."""
from celery import Celery, Task
def celery_init_app(app) -> Celery: def celery_init_app(app) -> Celery:
"""Initialize Celery with Flask application context."""
class FlaskTask(Task): class FlaskTask(Task):
"""Celery task that runs within Flask app context."""
def __call__(self, *args: object, **kwargs: object) -> object: def __call__(self, *args: object, **kwargs: object) -> object:
with app.app_context(): with app.app_context():
return self.run(*args, **kwargs) return self.run(*args, **kwargs)
@@ -14,6 +19,10 @@ def celery_init_app(app) -> Celery:
'schedule': 30.0, 'schedule': 30.0,
}, },
} }
celery_app.conf.include = [
'celery_tasks.preferences',
'celery_tasks.streaming',
]
celery_app.set_default() celery_app.set_default()
app.extensions["celery"] = celery_app app.extensions["celery"] = celery_app
return celery_app return celery_app

View File

@@ -1,3 +1,5 @@
"""Celery app initialization with Flask."""
from blueprints import create_app from blueprints import create_app
flask_app = create_app() flask_app = create_app()

View File

@@ -1,15 +1,19 @@
"""Scheduled task for updating user preferences based on stream viewing."""
import json
from celery import shared_task from celery import shared_task
from database.database import Database from database.database import Database
import redis import redis
import json
redis_url = "redis://redis:6379/1" REDIS_URL = "redis://redis:6379/1"
r = redis.from_url(redis_url, decode_responses=True) r = redis.from_url(REDIS_URL, decode_responses=True)
@shared_task @shared_task
def user_preferences(): def user_preferences():
""" """
Updates users preferences on different stream categories based on the streams they are currently watching Updates users preferences on different stream categories
based on the streams they are currently watching
""" """
stats = r.hget("current_viewers", "viewers") stats = r.hget("current_viewers", "viewers")
# If there are any current viewers # If there are any current viewers
@@ -21,13 +25,19 @@ def user_preferences():
# For each user and stream combination # For each user and stream combination
for stream_id in stream_ids: for stream_id in stream_ids:
# Retrieves category associated with stream # Retrieves category associated with stream
current_category = db.fetchone("""SELECT category_id FROM streams current_category = db.fetchone(
WHERE user_id = ? """SELECT category_id FROM streams
""", (stream_id,)) WHERE user_id = ?
# If stream is still live then update the user_preferences table to reflect their preferences """, (stream_id,))
# If stream is still live then update the
# user_preferences table to reflect their preferences
if current_category: if current_category:
db.execute("""INSERT INTO user_preferences (user_id,category_id,favourability) db.execute(
VALUES (?,?,?) """INSERT INTO user_preferences
ON CONFLICT(user_id, category_id) (user_id,category_id,favourability)
DO UPDATE SET favourability = favourability + 1 VALUES (?,?,?)
""", (user_id, current_category["category_id"], 1)) ON CONFLICT(user_id, category_id)
DO UPDATE SET
favourability = favourability + 1
""", (user_id,
current_category["category_id"], 1))

View File

@@ -1,11 +1,14 @@
from celery import Celery, shared_task, Task """Async tasks for stream thumbnail updates, VOD creation, and image conversion."""
from datetime import datetime
from celery_tasks.preferences import user_preferences
from utils.stream_utils import generate_thumbnail, get_streamer_live_status, get_custom_thumbnail_status, remove_hls_files, get_video_duration
from time import sleep
from os import listdir, remove
from utils.path_manager import PathManager
import subprocess import subprocess
from os import listdir
from celery import shared_task
from utils.stream_utils import (
generate_thumbnail, get_streamer_live_status,
get_custom_thumbnail_status, remove_hls_files, get_video_duration
)
from utils.path_manager import PathManager
path_manager = PathManager() path_manager = PathManager()
@@ -16,10 +19,13 @@ def update_thumbnail(user_id, stream_file, thumbnail_file, sleep_time, second_ca
""" """
# Check if stream is still live and custom thumbnail has not been set # Check if stream is still live and custom thumbnail has not been set
if get_streamer_live_status(user_id)['is_live'] and not get_custom_thumbnail_status(user_id)['custom_thumbnail']: if (get_streamer_live_status(user_id)['is_live']
and not get_custom_thumbnail_status(user_id)['custom_thumbnail']):
print("Updating thumbnail...") print("Updating thumbnail...")
generate_thumbnail(stream_file, thumbnail_file) generate_thumbnail(stream_file, thumbnail_file)
update_thumbnail.apply_async((user_id, stream_file, thumbnail_file, sleep_time, second_capture), countdown=sleep_time) update_thumbnail.apply_async(
(user_id, stream_file, thumbnail_file, sleep_time, second_capture),
countdown=sleep_time)
else: else:
print(f"Stopping thumbnail updates for stream of {user_id}") print(f"Stopping thumbnail updates for stream of {user_id}")
@@ -34,7 +40,7 @@ def combine_ts_stream(stream_path, vods_path, vod_file_name, thumbnail_file) ->
ts_files.sort() ts_files.sort()
# Create temp file listing all ts files # Create temp file listing all ts files
with open(f"{stream_path}/list.txt", "w") as f: with open(f"{stream_path}/list.txt", "w", encoding="utf-8") as f:
for ts_file in ts_files: for ts_file in ts_files:
f.write(f"file '{ts_file}'\n") f.write(f"file '{ts_file}'\n")
@@ -53,7 +59,7 @@ def combine_ts_stream(stream_path, vods_path, vod_file_name, thumbnail_file) ->
vod_file_path vod_file_path
] ]
subprocess.run(vod_command) subprocess.run(vod_command, check=True)
# Remove HLS files, even if user is not streaming # Remove HLS files, even if user is not streaming
remove_hls_files(stream_path) remove_hls_files(stream_path)
@@ -78,4 +84,4 @@ def convert_image_to_png(image_path, png_path):
png_path png_path
] ]
subprocess.run(image_command) subprocess.run(image_command, check=True)

View File

@@ -1,7 +1,12 @@
"""SQLite database connection management with context manager support."""
import sqlite3 import sqlite3
import os import os
class Database: class Database:
"""Database wrapper providing connection management and query execution."""
def __init__(self) -> None: def __init__(self) -> None:
self._db = os.path.join(os.path.abspath(os.path.dirname(__file__)), "app.db") self._db = os.path.join(os.path.abspath(os.path.dirname(__file__)), "app.db")
self._conn = None self._conn = None
@@ -63,4 +68,6 @@ class Database:
if not result: if not result:
return [] return []
columns = [desc[0] for desc in self.cursor.description] columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in result] if isinstance(result, list) else dict(zip(columns, result)) if isinstance(result, list):
return [dict(zip(columns, row)) for row in result]
return dict(zip(columns, result))

View File

@@ -1,8 +1,10 @@
"""Admin utility functions for user management."""
from database.database import Database from database.database import Database
def check_if_admin(username): def check_if_admin(username):
""" """
Returns whether user is admin Returns whether user is admin
""" """
with Database() as db: with Database() as db:
is_admin = db.fetchone(""" is_admin = db.fetchone("""
@@ -34,5 +36,4 @@ def ban_user(banned_user):
db.execute(""" db.execute("""
DELETE FROM users DELETE FROM users
WHERE username = ?;""", WHERE username = ?;""",
(banned_user) (banned_user,))
)

View File

@@ -1,13 +1,17 @@
"""Token generation and verification for password resets."""
from typing import Optional
from os import getenv
from database.database import Database from database.database import Database
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from typing import Optional
from dotenv import load_dotenv from dotenv import load_dotenv
from os import getenv
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
load_dotenv() load_dotenv()
serializer = URLSafeTimedSerializer(getenv("AUTH_SECRET_KEY")) serializer = URLSafeTimedSerializer(getenv("AUTH_SECRET_KEY"))
def generate_token(email, salt_value) -> str: def generate_token(email, salt_value) -> str:
""" """
Creates a token for password reset Creates a token for password reset
@@ -19,7 +23,6 @@ def verify_token(token: str, salt_value) -> Optional[str]:
""" """
Given a token, verifies and decodes it into an email Given a token, verifies and decodes it into an email
""" """
try: try:
email = serializer.loads(token, salt=salt_value, max_age=3600) email = serializer.loads(token, salt=salt_value, max_age=3600)
return email return email

View File

@@ -1,16 +1,18 @@
"""Email sending utilities for password reset, account confirmation, and newsletters."""
import smtplib import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from os import getenv from os import getenv
from secrets import token_hex
from dotenv import load_dotenv from dotenv import load_dotenv
from utils.auth import generate_token from utils.auth import generate_token
from secrets import token_hex
from .user_utils import get_session_info_email from .user_utils import get_session_info_email
import redis import redis
from database.database import Database from database.database import Database
redis_url = "redis://redis:6379/1" REDIS_URL = "redis://redis:6379/1"
r = redis.from_url(redis_url, decode_responses=True) r = redis.from_url(REDIS_URL, decode_responses=True)
load_dotenv() load_dotenv()
@@ -23,31 +25,31 @@ def send_email(email, func) -> None:
""" """
# Setup the sender email details # Setup the sender email details
SMTP_SERVER = "smtp.gmail.com" smtp_server = "smtp.gmail.com"
SMTP_PORT = 587 smtp_port = 587
SMTP_EMAIL = getenv("EMAIL") smtp_email = getenv("EMAIL")
SMTP_PASSWORD = getenv("EMAIL_PASSWORD") smtp_password = getenv("EMAIL_PASSWORD")
# Setup up the receiver details # Setup up the receiver details
body, subject = func() body, subject = func()
msg = MIMEText(body, "html") msg = MIMEText(body, "html")
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = SMTP_EMAIL msg["From"] = smtp_email
msg["To"] = email msg["To"] = email
# Send the email using smtplib # Send the email using smtplib
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp: with smtplib.SMTP(smtp_server, smtp_port) as smtp:
try: try:
smtp.starttls() # TLS handshake to start the connection smtp.starttls() # TLS handshake to start the connection
smtp.login(SMTP_EMAIL, SMTP_PASSWORD) smtp.login(smtp_email, smtp_password)
smtp.ehlo() smtp.ehlo()
smtp.send_message(msg) smtp.send_message(msg)
except TimeoutError: except TimeoutError:
print("Server timed out", flush=True) print("Server timed out", flush=True)
except Exception as e: except smtplib.SMTPException as e:
print("Error: ", e, flush=True) print("Error: ", e, flush=True)
def forgot_password_body(email) -> str: def forgot_password_body(email) -> str:

View File

@@ -1,8 +1,11 @@
"""Description: This file contains the PathManager class which is responsible for managing the paths of the stream data.""" """File system path management for user streams, VODs, and profile pictures."""
import os import os
class PathManager(): class PathManager():
"""Manages paths for user stream data, VODs, and profile pictures."""
def __init__(self) -> None: def __init__(self) -> None:
self.root_path = "user_data" self.root_path = "user_data"
self.vod_directory_name = "vods" self.vod_directory_name = "vods"
@@ -39,25 +42,33 @@ class PathManager():
os.rmdir(user_path) os.rmdir(user_path)
def get_user_path(self, username): def get_user_path(self, username):
"""Returns the base path for a user's data directory."""
return os.path.join(self.root_path, username) return os.path.join(self.root_path, username)
def get_vods_path(self, username): def get_vods_path(self, username):
"""Returns the path to a user's VODs directory."""
return os.path.join(self.root_path, username, self.vod_directory_name) return os.path.join(self.root_path, username, self.vod_directory_name)
def get_stream_path(self, username): def get_stream_path(self, username):
"""Returns the path to a user's stream directory."""
return os.path.join(self.root_path, username, self.stream_directory_name) return os.path.join(self.root_path, username, self.stream_directory_name)
def get_stream_file_path(self, username): def get_stream_file_path(self, username):
"""Returns the path to a user's stream index file."""
return os.path.join(self.get_stream_path(username), "index.m3u8") return os.path.join(self.get_stream_path(username), "index.m3u8")
def get_current_stream_thumbnail_file_path(self, username): def get_current_stream_thumbnail_file_path(self, username):
"""Returns the path to a user's current stream thumbnail."""
return os.path.join(self.get_stream_path(username), "index.png") return os.path.join(self.get_stream_path(username), "index.png")
def get_vod_file_path(self, username, vod_id): def get_vod_file_path(self, username, vod_id):
"""Returns the path to a specific VOD file."""
return os.path.join(self.get_vods_path(username), f"{vod_id}.mp4") return os.path.join(self.get_vods_path(username), f"{vod_id}.mp4")
def get_vod_thumbnail_file_path(self, username, vod_id): def get_vod_thumbnail_file_path(self, username, vod_id):
"""Returns the path to a specific VOD thumbnail."""
return os.path.join(self.get_vods_path(username), f"{vod_id}.png") return os.path.join(self.get_vods_path(username), f"{vod_id}.png")
def get_profile_picture_file_path(self, username): def get_profile_picture_file_path(self, username):
"""Returns the path to a user's profile picture."""
return os.path.join(self.root_path, username, self.profile_picture_name) return os.path.join(self.root_path, username, self.profile_picture_name)

View File

@@ -1,9 +1,13 @@
from database.database import Database """Personalized stream and category recommendations based on user preferences."""
from typing import Optional, List from typing import Optional, List
from database.database import Database
def get_user_preferred_category(user_id: int) -> Optional[int]: def get_user_preferred_category(user_id: int) -> Optional[int]:
""" """
Queries user_preferences database to find users favourite streaming category and returns the category Queries user_preferences database to find users favourite
streaming category and returns the category
""" """
with Database() as db: with Database() as db:
category = db.fetchone(""" category = db.fetchone("""
@@ -16,17 +20,23 @@ def get_user_preferred_category(user_id: int) -> Optional[int]:
return category["category_id"] if category else None return category["category_id"] if category else None
def get_followed_categories_recommendations(user_id: int, no_streams: int = 4) -> Optional[List[dict]]: def get_followed_categories_recommendations(
user_id: int, no_streams: int = 4
) -> Optional[List[dict]]:
""" """
Returns top streams given a user's category following Returns top streams given a user's category following
""" """
with Database() as db: with Database() as db:
streams = db.fetchall(""" streams = db.fetchall("""
SELECT u.user_id, title, u.username, num_viewers, category_name SELECT u.user_id, title, u.username, num_viewers,
category_name
FROM streams FROM streams
JOIN users u ON streams.user_id = u.user_id JOIN users u ON streams.user_id = u.user_id
JOIN categories ON streams.category_id = categories.category_id JOIN categories
WHERE categories.category_id IN (SELECT category_id FROM followed_categories WHERE user_id = ?) ON streams.category_id = categories.category_id
WHERE categories.category_id IN
(SELECT category_id
FROM followed_categories WHERE user_id = ?)
ORDER BY num_viewers DESC ORDER BY num_viewers DESC
LIMIT ?; LIMIT ?;
""", (user_id, no_streams)) """, (user_id, no_streams))
@@ -47,13 +57,17 @@ def get_followed_your_categories(user_id: int) -> Optional[List[dict]]:
return categories return categories
def get_streams_based_on_category(category_id: int, no_streams: int = 4, offset: int = 0) -> Optional[List[dict]]: def get_streams_based_on_category(
category_id: int, no_streams: int = 4, offset: int = 0
) -> Optional[List[dict]]:
""" """
Queries stream database to get top most viewed streams based on given category Queries stream database to get top most viewed streams
based on given category
""" """
with Database() as db: with Database() as db:
streams = db.fetchall(""" streams = db.fetchall("""
SELECT u.user_id, title, username, num_viewers, c.category_name SELECT u.user_id, title, username, num_viewers,
c.category_name
FROM streams s FROM streams s
JOIN users u ON s.user_id = u.user_id JOIN users u ON s.user_id = u.user_id
JOIN categories c ON s.category_id = c.category_id JOIN categories c ON s.category_id = c.category_id
@@ -70,42 +84,62 @@ def get_highest_view_streams(no_streams: int = 4) -> Optional[List[dict]]:
""" """
with Database() as db: with Database() as db:
data = db.fetchall(""" data = db.fetchall("""
SELECT u.user_id, username, title, num_viewers, category_name SELECT u.user_id, username, title, num_viewers,
category_name
FROM streams FROM streams
JOIN users u ON streams.user_id = u.user_id JOIN users u ON streams.user_id = u.user_id
JOIN categories ON streams.category_id = categories.category_id JOIN categories
ON streams.category_id = categories.category_id
ORDER BY num_viewers DESC ORDER BY num_viewers DESC
LIMIT ?; LIMIT ?;
""", (no_streams,)) """, (no_streams,))
return data return data
def get_highest_view_categories(no_categories: int = 4, offset: int = 0) -> Optional[List[dict]]: def get_highest_view_categories(
no_categories: int = 4, offset: int = 0
) -> Optional[List[dict]]:
""" """
Returns a list of top most popular categories given offset Returns a list of top most popular categories given offset
""" """
with Database() as db: with Database() as db:
categories = db.fetchall(""" categories = db.fetchall("""
SELECT categories.category_id, categories.category_name, COALESCE(SUM(streams.num_viewers), 0) AS num_viewers SELECT categories.category_id,
categories.category_name,
COALESCE(SUM(streams.num_viewers), 0)
AS num_viewers
FROM categories FROM categories
LEFT JOIN streams ON streams.category_id = categories.category_id LEFT JOIN streams
GROUP BY categories.category_id, categories.category_name ON streams.category_id = categories.category_id
GROUP BY categories.category_id,
categories.category_name
ORDER BY num_viewers DESC ORDER BY num_viewers DESC
LIMIT ? OFFSET ?; LIMIT ? OFFSET ?;
""", (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
""" """
with Database() as db: with Database() as db:
categories = db.fetchall(""" categories = db.fetchall("""
SELECT categories.category_id, categories.category_name, COALESCE(SUM(streams.num_viewers), 0) AS num_viewers SELECT categories.category_id,
categories.category_name,
COALESCE(SUM(streams.num_viewers), 0)
AS num_viewers
FROM categories FROM categories
JOIN user_preferences ON categories.category_id = user_preferences.category_id JOIN user_preferences
LEFT JOIN streams ON categories.category_id = streams.category_id ON categories.category_id
= user_preferences.category_id
LEFT JOIN streams
ON categories.category_id
= streams.category_id
WHERE user_preferences.user_id = ? WHERE user_preferences.user_id = ?
GROUP BY categories.category_id, categories.category_name GROUP BY categories.category_id,
categories.category_name
ORDER BY user_preferences.favourability DESC ORDER BY user_preferences.favourability DESC
LIMIT ? LIMIT ?
""", (user_id, no_categories)) """, (user_id, no_categories))

View File

@@ -1,9 +1,11 @@
from database.database import Database """Stream data retrieval and management utilities."""
from typing import Optional
import os, subprocess import os
import subprocess
from typing import Optional, List from typing import Optional, List
from time import sleep
from utils.path_manager import PathManager from database.database import Database
def get_streamer_live_status(user_id: int): def get_streamer_live_status(user_id: int):
""" """
@@ -21,17 +23,19 @@ def get_streamer_live_status(user_id: int):
def get_followed_live_streams(user_id: int) -> Optional[List[dict]]: def get_followed_live_streams(user_id: int) -> Optional[List[dict]]:
""" """
Searches for streamers who the user followed which are currently live Searches for streamers who the user followed which are currently live
Returns a list of live streams with the streamer's user id, stream title, and number of viewers Returns a list of live streams with the streamer's user id,
stream title, and number of viewers
""" """
with Database() as db: with Database() as db:
live_streams = db.fetchall(""" live_streams = db.fetchall("""
SELECT users.user_id, streams.title, streams.num_viewers, users.username SELECT users.user_id, streams.title,
FROM streams JOIN users streams.num_viewers, users.username
ON streams.user_id = users.user_id FROM streams JOIN users
WHERE users.user_id IN ON streams.user_id = users.user_id
(SELECT followed_id FROM follows WHERE user_id = ?) WHERE users.user_id IN
AND users.is_live = 1; (SELECT followed_id FROM follows WHERE user_id = ?)
""", (user_id,)) AND users.is_live = 1;
""", (user_id,))
return live_streams return live_streams
def get_current_stream_data(user_id: int) -> Optional[dict]: def get_current_stream_data(user_id: int) -> Optional[dict]:
@@ -40,7 +44,8 @@ def get_current_stream_data(user_id: int) -> Optional[dict]:
""" """
with Database() as db: with Database() as db:
most_recent_stream = db.fetchone(""" most_recent_stream = db.fetchone("""
SELECT s.user_id, u.username, s.title, s.start_time, s.num_viewers, c.category_name, c.category_id SELECT s.user_id, u.username, s.title, s.start_time,
s.num_viewers, c.category_name, c.category_id
FROM streams AS s FROM streams AS s
JOIN categories AS c ON s.category_id = c.category_id JOIN categories AS c ON s.category_id = c.category_id
JOIN users AS u ON s.user_id = u.user_id JOIN users AS u ON s.user_id = u.user_id
@@ -60,69 +65,83 @@ def end_user_stream(stream_key, user_id, username):
Returns: Returns:
bool: True if stream was ended successfully, False otherwise bool: True if stream was ended successfully, False otherwise
""" """
from flask import current_app
from datetime import datetime from datetime import datetime
from dateutil import parser from dateutil import parser
from celery_tasks.streaming import combine_ts_stream from celery_tasks.streaming import combine_ts_stream
from utils.path_manager import PathManager from utils.path_manager import PathManager
path_manager = PathManager() pm = PathManager()
print(f"Ending stream for user {username} (ID: {user_id})", flush=True) print(f"Ending stream for user {username} (ID: {user_id})", flush=True)
if not stream_key or not user_id or not username: if not stream_key or not user_id or not username:
print("Cannot end stream - missing required information", flush=True) print("Cannot end stream - missing required information", flush=True)
return False return False, "Missing required information"
try: try:
# Open database connection # Open database connection
with Database() as db: with Database() as db:
# Get stream info # Get stream info
stream_info = db.fetchone("""SELECT * stream_info = db.fetchone(
FROM streams """SELECT *
WHERE user_id = ?""", (user_id,)) FROM streams
WHERE user_id = ?""", (user_id,))
# If user is not streaming, just return # If user is not streaming, just return
if not stream_info: if not stream_info:
print(f"User {username} (ID: {user_id}) is not streaming", flush=True) print(
f"User {username} (ID: {user_id}) "
"is not streaming", flush=True)
return True, "User is not streaming" return True, "User is not streaming"
# Remove stream from database # Remove stream from database
db.execute("""DELETE FROM streams db.execute(
WHERE user_id = ?""", (user_id,)) """DELETE FROM streams
WHERE user_id = ?""", (user_id,))
# Move stream to vod table # Move stream to vod table
stream_length = int( stream_length = int(
(datetime.now() - parser.parse(stream_info.get("start_time"))).total_seconds()) (datetime.now() - parser.parse(
stream_info.get("start_time")
)).total_seconds())
db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views) db.execute(
VALUES (?, ?, ?, ?, ?, ?)""", (user_id, """INSERT INTO vods
stream_info.get("title"), (user_id, title, datetime, category_id,
stream_info.get("start_time"), length, views)
stream_info.get("category_id"), VALUES (?, ?, ?, ?, ?, ?)""",
stream_length, (user_id,
0)) stream_info.get("title"),
stream_info.get("start_time"),
stream_info.get("category_id"),
stream_length,
0))
vod_id = db.get_last_insert_id() vod_id = db.get_last_insert_id()
# Set user as not streaming # Set user as not streaming
db.execute("""UPDATE users db.execute(
SET is_live = 0 """UPDATE users
WHERE user_id = ?""", (user_id,)) SET is_live = 0
WHERE user_id = ?""", (user_id,))
# Queue task to combine TS files into MP4 # Queue task to combine TS files into MP4
combine_ts_stream.delay( combine_ts_stream.delay(
path_manager.get_stream_path(username), pm.get_stream_path(username),
path_manager.get_vods_path(username), pm.get_vods_path(username),
vod_id, vod_id,
path_manager.get_vod_thumbnail_file_path(username, vod_id) pm.get_vod_thumbnail_file_path(username, vod_id)
) )
print(f"Stream ended for user {username} (ID: {user_id})", flush=True) print(
f"Stream ended for user {username} (ID: {user_id})",
flush=True)
return True, "Stream ended successfully" return True, "Stream ended successfully"
except Exception as e: except (ValueError, TypeError, KeyError) as exc:
print(f"Error ending stream for user {username}: {str(e)}", flush=True) print(
return False, f"Error ending stream: {str(e)}" f"Error ending stream for user {username}: {str(exc)}",
flush=True)
return False, f"Error ending stream: {str(exc)}"
def get_category_id(category_name: str) -> Optional[int]: def get_category_id(category_name: str) -> Optional[int]:
""" """
@@ -153,7 +172,13 @@ 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 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,)) 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):
@@ -161,7 +186,14 @@ 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.*, 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,)) 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):
@@ -169,10 +201,17 @@ 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.*, 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,)) 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 return vods
def generate_thumbnail(stream_file: str, thumbnail_file: str, second_capture) -> None: def generate_thumbnail(stream_file: str, thumbnail_file: str, second_capture=0) -> None:
""" """
Generates the thumbnail of a stream Generates the thumbnail of a stream
""" """
@@ -192,10 +231,16 @@ def generate_thumbnail(stream_file: str, thumbnail_file: str, second_capture) ->
] ]
try: try:
subprocess.run(thumbnail_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) subprocess.run(
thumbnail_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True)
print(f"Thumbnail generated for {stream_file}") print(f"Thumbnail generated for {stream_file}")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError:
print(f"No information available for {stream_file}, aborting thumbnail generation") print(
f"No information available for {stream_file}, "
"aborting thumbnail generation")
def remove_hls_files(stream_path: str) -> None: def remove_hls_files(stream_path: str) -> None:
""" """
@@ -221,11 +266,13 @@ def get_video_duration(video_path: str) -> int:
] ]
try: try:
video_length = subprocess.check_output(video_length_command).decode("utf-8") video_length = subprocess.check_output(
video_length_command
).decode("utf-8")
print(f"Video length: {video_length}") print(f"Video length: {video_length}")
return int(float(video_length)) return int(float(video_length))
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError:
print(f"Error getting video length: {e}") print("Error getting video length")
return 0 return 0
def get_stream_tags(user_id: int) -> Optional[List[str]]: def get_stream_tags(user_id: int) -> Optional[List[str]]:

View File

@@ -1,8 +1,12 @@
from database.database import Database """User profile management, following, and subscription utilities."""
from typing import Optional, List from typing import Optional, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
from database.database import Database
from dateutil import parser from dateutil import parser
def get_user_id(username: str) -> Optional[int]: def get_user_id(username: str) -> Optional[int]:
""" """
Returns user_id associated with given username Returns user_id associated with given username
@@ -40,7 +44,8 @@ def update_bio(user_id: int, bio: str):
def has_password(email: str): def has_password(email: str):
""" """
Returns if account associated with this email has password, i.e is account from Google OAuth Returns if account associated with this email has password,
i.e is account from Google OAuth
""" """
with Database() as db: with Database() as db:
data = db.fetchone(""" data = db.fetchone("""
@@ -48,7 +53,7 @@ def has_password(email: str):
FROM users FROM users
WHERE email = ? WHERE email = ?
""", (email,)) """, (email,))
return False if data["password"] == None else True return data["password"] is not None
def get_session_info_email(email: str) -> dict: def get_session_info_email(email: str) -> dict:
""" """
@@ -227,6 +232,7 @@ def subscription_expiration(user_id: int, subscribed_id: int) -> int:
return 0 return 0
def get_email(user_id: int) -> Optional[str]: def get_email(user_id: int) -> Optional[str]:
"""Returns the email address for a given user_id."""
with Database() as db: with Database() as db:
email = db.fetchone(""" email = db.fetchone("""
SELECT email SELECT email

View File

@@ -1,7 +1,11 @@
from database.database import Database """Input sanitization and validation utilities."""
from typing import Optional, List from typing import Optional, List
from re import match from re import match
from database.database import Database
def get_all_categories() -> Optional[List[dict]]: def get_all_categories() -> Optional[List[dict]]:
""" """
Returns all possible streaming categories Returns all possible streaming categories
@@ -84,10 +88,10 @@ def sanitize(user_input: str, input_type="default") -> str:
} }
# Get the validation rules for the specified type # Get the validation rules for the specified type
r = rules.get(input_type) rule = rules.get(input_type)
if not r or \ if (not rule
not (r["min_length"] <= len(sanitised_input) <= r["max_length"]) or \ or not (rule["min_length"] <= len(sanitised_input) <= rule["max_length"])
not match(r["pattern"], sanitised_input): or not match(rule["pattern"], sanitised_input)):
raise ValueError("Unaccepted character or length in input") raise ValueError("Unaccepted character or length in input")
return sanitised_input return sanitised_input