Fix/pylint cleanup (#8)
* 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:
committed by
GitHub
parent
fed1a2f288
commit
2758be8680
24
frontend/package-lock.json
generated
24
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""WebSocket configuration using Flask-SocketIO."""
|
||||||
|
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
|
|
||||||
socketio = SocketIO(
|
socketio = SocketIO(
|
||||||
|
|||||||
@@ -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,7 +246,8 @@ 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(
|
||||||
|
"""SELECT user_id, username, is_live
|
||||||
FROM users
|
FROM users
|
||||||
WHERE stream_key = ?""", (stream_key,))
|
WHERE stream_key = ?""", (stream_key,))
|
||||||
|
|
||||||
@@ -280,24 +295,26 @@ 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(
|
||||||
|
"""SELECT user_id, username, is_live
|
||||||
FROM users
|
FROM users
|
||||||
WHERE stream_key = ?""", (stream_key,))
|
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
|
||||||
@@ -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(
|
||||||
|
(user_id,
|
||||||
path_manager.get_stream_file_path(username),
|
path_manager.get_stream_file_path(username),
|
||||||
path_manager.get_current_stream_thumbnail_file_path(username),
|
path_manager.get_current_stream_thumbnail_file_path(username),
|
||||||
THUMBNAIL_GENERATION_INTERVAL), countdown=10)
|
THUMBNAIL_GENERATION_INTERVAL),
|
||||||
|
countdown=10)
|
||||||
|
|
||||||
return "OK", 200
|
return "OK", 200
|
||||||
|
|
||||||
@@ -332,28 +351,33 @@ 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(
|
||||||
|
"""SELECT user_id, username, is_live
|
||||||
FROM users
|
FROM users
|
||||||
WHERE stream_key = ?""", (stream_key,))
|
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(
|
||||||
|
"""UPDATE streams
|
||||||
SET title = ?, category_id = ?
|
SET title = ?, category_id = ?
|
||||||
WHERE user_id = ?""", (stream_title, get_category_id(stream_category), user_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(
|
||||||
|
"""UPDATE streams
|
||||||
SET custom_thumbnail = ?
|
SET custom_thumbnail = ?
|
||||||
WHERE user_id = ?""", (True, user_id))
|
WHERE user_id = ?""", (True, user_id))
|
||||||
|
|
||||||
@@ -390,7 +414,8 @@ 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(
|
||||||
|
"""SELECT user_id, username
|
||||||
FROM users
|
FROM users
|
||||||
WHERE stream_key = ?""", (stream_key,))
|
WHERE stream_key = ?""", (stream_key,))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
"""SELECT category_id FROM streams
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
""", (stream_id,))
|
""", (stream_id,))
|
||||||
# If stream is still live then update the user_preferences table to reflect their preferences
|
# 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(
|
||||||
|
"""INSERT INTO user_preferences
|
||||||
|
(user_id,category_id,favourability)
|
||||||
VALUES (?,?,?)
|
VALUES (?,?,?)
|
||||||
ON CONFLICT(user_id, category_id)
|
ON CONFLICT(user_id, category_id)
|
||||||
DO UPDATE SET favourability = favourability + 1
|
DO UPDATE SET
|
||||||
""", (user_id, current_category["category_id"], 1))
|
favourability = favourability + 1
|
||||||
|
""", (user_id,
|
||||||
|
current_category["category_id"], 1))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""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):
|
||||||
@@ -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,))
|
||||||
)
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,11 +23,13 @@ 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,
|
||||||
|
streams.num_viewers, users.username
|
||||||
FROM streams JOIN users
|
FROM streams JOIN users
|
||||||
ON streams.user_id = users.user_id
|
ON streams.user_id = users.user_id
|
||||||
WHERE users.user_id IN
|
WHERE users.user_id IN
|
||||||
@@ -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,42 +65,51 @@ 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(
|
||||||
|
"""SELECT *
|
||||||
FROM streams
|
FROM streams
|
||||||
WHERE user_id = ?""", (user_id,))
|
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(
|
||||||
|
"""DELETE FROM streams
|
||||||
WHERE user_id = ?""", (user_id,))
|
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
|
||||||
|
(user_id, title, datetime, category_id,
|
||||||
|
length, views)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||||
|
(user_id,
|
||||||
stream_info.get("title"),
|
stream_info.get("title"),
|
||||||
stream_info.get("start_time"),
|
stream_info.get("start_time"),
|
||||||
stream_info.get("category_id"),
|
stream_info.get("category_id"),
|
||||||
@@ -105,24 +119,29 @@ def end_user_stream(stream_key, user_id, username):
|
|||||||
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(
|
||||||
|
"""UPDATE users
|
||||||
SET is_live = 0
|
SET is_live = 0
|
||||||
WHERE user_id = ?""", (user_id,))
|
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]]:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user