Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified optifit backend/__pycache__/app.cpython-310.pyc
Binary file not shown.
Binary file modified optifit backend/__pycache__/squat_counter.cpython-310.pyc
Binary file not shown.
110 changes: 70 additions & 40 deletions optifit backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,38 @@
pass
else:
ssl._create_default_https_context = _create_unverified_https_context

from flask import Flask, request, send_file, jsonify, url_for
import os
import threading
import uuid
import time
from werkzeug.utils import secure_filename
from squat_counter import process_squat_video # Import the actual processing logic
from validation import *
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

UPLOAD_FOLDER = 'uploads'
PROCESSED_FOLDER = 'processed'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(PROCESSED_FOLDER, exist_ok=True)


# Creates standardised error response
def error_response(message, status_code):
return jsonify({
"success": False,
"error": True,
"message": message,
"status_code": status_code,
"timestamp": int(time.time())
}), status_code

# In-memory job store: {job_id: {"status": "processing"/"done", "result": {...}}}
jobs = {}

Expand All @@ -40,7 +57,7 @@ def process_video_async(job_id, input_path, output_path, video_url):
jobs[job_id]["error"] = str(e)
print(f"Error in background processing: {e}")


#Route to home
@app.route('/', methods=['GET'])
def home():
base_info = {
Expand All @@ -51,57 +68,70 @@ def home():
"/result/<job_id>": "GET - Check processing status and get results"
}
}

return base_info, 200

return jsonify(base_info), 200

#Route to ping the server
@app.route('/ping', methods=['GET'])
def ping():
return {"message": "Server is live!"}, 200
return jsonify({"message": "Server is live!"}), 200

#Route to get upload the video
@app.route('/upload', methods=['POST'])
def upload_video():
if 'video' not in request.files:
return {'error': 'No video file part'}, 400
try:
video = validate_upload_request(request)
validate_video_file(video)

video = request.files['video']
filename = secure_filename(video.filename)
input_path = os.path.join(UPLOAD_FOLDER, filename)
output_path = os.path.join(PROCESSED_FOLDER, f"processed_{filename}")

# Save uploaded video
video.save(input_path)

# Generate video URL
video_url = url_for('get_processed_video', filename=f"processed_{filename}", _external=True)

# Create job
job_id = str(uuid.uuid4())
jobs[job_id] = {"status": "processing"}

# Start background processing with pre-generated URL
threading.Thread(target=process_video_async, args=(job_id, input_path, output_path, video_url)).start()
filename = secure_filename(video.filename)
input_path = os.path.join(UPLOAD_FOLDER, filename)
output_path = os.path.join(PROCESSED_FOLDER, f"processed_{filename}")

# Save uploaded video
video.save(input_path)

# Generate video URL
video_url = url_for('get_processed_video', filename=f"processed_{filename}", _external=True)

# Create job
job_id = str(uuid.uuid4())
jobs[job_id] = {"status": "processing"}

# Start background processing with pre-generated URL
threading.Thread(target=process_video_async, args=(job_id, input_path, output_path, video_url)).start()

response_data = {
"status": "processing",
"job_id": job_id,
"message": "Video uploaded successfully. Processing started.",
"video_url": video_url
}

return jsonify(response_data)

response_data = {
"status": "processing",
"job_id": job_id,
"message": "Video uploaded successfully. Processing started.",
"video_url": video_url
}
except APIError as e:
logger.error(f"API Error in {request.endpoint}: {e.message}", exc_info=True)
return error_response(e.message, e.status_code)

return jsonify(response_data)


# Route to get the result of the job with the job id
@app.route('/result/<job_id>', methods=['GET'])
def get_result(job_id):
job = jobs.get(job_id)
if not job:
return jsonify({"status": "not_found", "error": "Job not found"}), 404

if job["status"] == "processing":
return jsonify({"status": "processing", "message": "Video is being processed..."})
elif job["status"] == "error":
return jsonify({"status": "error", "error": job.get("error", "Unknown error")}), 500
else:
return jsonify({"status": "done", "result": job["result"]})
try:
validate_job_request(job_id,jobs)

job = jobs.get(job_id)
if job["status"] == "processing":
return jsonify({"status": "processing", "message": "Video is being processed..."})
elif job["status"] == "error":
raise InternalServerError("Unknown Error")
else:
return jsonify({"status": "done", "result": job["result"]})

except APIError as e:
logger.error(f"API Error in {request.endpoint}: {e.message}", exc_info=True)
return error_response(e.message, e.status_code)

# New endpoint to serve processed videos by filename
@app.route('/processed/<filename>')
Expand Down
117 changes: 117 additions & 0 deletions optifit backend/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os
import uuid

class APIError(Exception):
def __init__(self, message, status_code=500):
self.message = message
self.status_code = status_code

class BadRequestError(APIError):
def __init__(self, message):
super().__init__(message, 400)

class NotFoundError(APIError):
def __init__(self, message):
super().__init__(message, 404)

class MethodNotAllowedError(APIError):
def __init__(self, message):
super().__init__(message, 405)

class UnsupportedMediaTypeError(APIError):
def __init__(self, message):
super().__init__(message, 415)

class InternalServerError(APIError):
def __init__(self, message):
super().__init__(message, 500)

class PayloadTooLargeError(APIError):
def __init__(self, message):
super().__init__(message, 413)


# Configuration constants
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB (removed extra * 1024)
ALLOWED_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv'}


# Validating the upload request for the file
def validate_upload_request(request):
# Checks included -> valid method, video field in request, if video is provided

if request.method != 'POST':
raise MethodNotAllowedError("Only POST method is allowed")

if not request.files:
raise BadRequestError("No files sent in request")

if 'video' not in request.files:
raise BadRequestError("No 'video' field found in request")

video = request.files['video']

if not video:
raise BadRequestError("No video file provided")

if not video.filename or video.filename.strip() == '':
raise BadRequestError("No video file selected")

return video


# Validates the video file iteslf in terms of file size, extensions and filename
def validate_video_file(file):
# Calculating the file size
file.seek(0, 2) # Seek to end
file_size = file.tell()
file.seek(0)

#Checks if the file size is within limits
if file_size == 0:
raise BadRequestError("Video file is empty")

if file_size > MAX_FILE_SIZE:
raise PayloadTooLargeError(
f"File size ({file_size / (1024*1024):.1f}MB) exceeds maximum "
f"allowed size ({MAX_FILE_SIZE / (1024*1024)}MB)"
)

if not file.filename:
raise BadRequestError("File has no name")


# Validates for given extensions
file_ext = os.path.splitext(file.filename)[1].lower()
if file_ext not in ALLOWED_EXTENSIONS:
raise UnsupportedMediaTypeError(
f"Unsupported file format '{file_ext}'. "
f"Allowed formats: {', '.join(ALLOWED_EXTENSIONS)}"
)


# Validating the job request for getting the results
def validate_job_request(job_id, jobs):
if not job_id:
raise BadRequestError("Job ID is required")

# Validate UUID format
try:
uuid.UUID(job_id)
except ValueError:
raise BadRequestError("Invalid job ID format")

job = jobs.get(job_id)
if not job:
raise NotFoundError("Job not found")


def validate_file_serving(filename):
if not filename:
raise BadRequestError("Filename is required")

if '..' in filename or '/' in filename or '\\' in filename:
raise BadRequestError("Invalid filename - path traversal not allowed")

return True

Loading