Fred808 commited on
Commit
3dbbc64
·
verified ·
1 Parent(s): 5a777f1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -57
app.py CHANGED
@@ -2,11 +2,16 @@ from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks, Q
2
  from fastapi.responses import FileResponse, JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  import os
5
- import shutil # Add missing import
6
  import uuid
7
  import time
8
  from typing import Dict, List, Optional
9
  import threading
 
 
 
 
 
10
 
11
  # Import processing functions and variables
12
  from processing_logic import (
@@ -25,14 +30,16 @@ app = FastAPI(title="MP4 Processing API", description="API for uploading, proces
25
  # Configure CORS
26
  app.add_middleware(
27
  CORSMiddleware,
28
- allow_origins=["*"], # Allows all origins
29
  allow_credentials=True,
30
- allow_methods=["*"], # Allows all methods
31
- allow_headers=["*"], # Allows all headers
32
  )
33
 
34
  # Define MP4_UPLOAD_FOLDER if not imported from processing_logic
35
- MP4_UPLOAD_FOLDER = os.path.join(UPLOAD_DIRECTORY, "uploads")
 
 
36
 
37
  processing_thread = None
38
 
@@ -43,6 +50,10 @@ def save_file(uploaded_file: UploadFile, save_path: str):
43
  with open(save_path, "wb") as f:
44
  shutil.copyfileobj(uploaded_file.file, f)
45
 
 
 
 
 
46
  # === ROUTES ===
47
 
48
  @app.get("/")
@@ -51,88 +62,149 @@ async def root():
51
  return {
52
  "message": "MP4 Processing API",
53
  "version": "1.0.0",
 
54
  "endpoints": {
55
  "upload": "POST /upload - Upload MP4 file",
56
- "download": "GET /download/{mp4_path:path} - Download MP4 file",
57
  "list": "GET /list - List uploaded files",
58
  "courses": "GET /courses - List all course folders",
59
  "images": "GET /images/{course_folder:path} - List MP4s in course",
60
- "start_processing": "POST /process/start - Start HF processing",
61
- "processing_status": "GET /process/status - Get processing status",
62
- "stop_processing": "POST /process/stop - Stop processing"
63
  }
64
  }
65
 
66
  @app.post("/upload/")
67
  async def upload_file(file: UploadFile = File(...)):
68
  """Uploads a file and stores it by filename inside the upload folder"""
 
 
 
 
 
69
  save_path = os.path.join(MP4_UPLOAD_FOLDER, file.filename)
70
  save_file(file, save_path)
71
- return {"status": "uploaded", "filename": file.filename}
 
 
72
 
73
  @app.get("/list")
74
  async def list_uploaded_files():
75
  """List uploaded MP4 files"""
 
 
76
  files = []
77
  if os.path.exists(MP4_UPLOAD_FOLDER):
78
  for file in os.listdir(MP4_UPLOAD_FOLDER):
79
  if file.lower().endswith(".mp4"):
80
- files.append(file)
 
 
 
 
 
 
 
81
  return JSONResponse(content=files)
82
 
83
- @app.get("/download/{mp4_path:path}")
84
- async def download_file(mp4_path: str):
 
85
  """
86
  Download any MP4 under processed_mp4s, including nested course folders.
87
- e.g. GET /download/MDS_CGAdventure_DownloadPirate.com/1.mp4
88
  """
89
- print(f"[DEBUG] Download request for: {mp4_path}")
 
 
90
 
91
- # Compose the full file path
92
- full_path = os.path.join(MP4_OUTPUT_FOLDER, mp4_path)
93
- print(f"[DEBUG] Full path: {full_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- # Security: Prevent escaping outside of MP4_OUTPUT_FOLDER
96
  full_path = os.path.normpath(full_path)
97
- if not full_path.startswith(os.path.abspath(MP4_OUTPUT_FOLDER)):
98
- print(f"[ERROR] Security violation - path outside output folder")
99
- raise HTTPException(status_code=400, detail="Invalid path")
 
 
100
 
101
- print(f"[DEBUG] Normalized path: {full_path}")
102
- print(f"[DEBUG] File exists: {os.path.exists(full_path)}")
103
- print(f"[DEBUG] Is MP4: {full_path.lower().endswith('.mp4')}")
104
 
105
  if not os.path.exists(full_path):
106
- # Try to find the file in subdirectories
107
- print(f"[DEBUG] File not found at expected path, searching...")
108
 
109
- # List what's actually in the MP4_OUTPUT_FOLDER
110
- if os.path.exists(MP4_OUTPUT_FOLDER):
111
- print(f"[DEBUG] Contents of {MP4_OUTPUT_FOLDER}:")
112
- for root, dirs, files in os.walk(MP4_OUTPUT_FOLDER):
113
- for file in files:
114
- if file.lower().endswith('.mp4'):
115
- rel_path = os.path.relpath(os.path.join(root, file), MP4_OUTPUT_FOLDER)
116
- print(f"[DEBUG] Found MP4: {rel_path}")
117
 
118
- raise HTTPException(status_code=404, detail=f"File not found: {mp4_path}")
119
-
120
- if not full_path.lower().endswith(".mp4"):
121
- raise HTTPException(status_code=400, detail="File is not an MP4")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- print(f"[DEBUG] Serving file: {full_path}")
 
 
124
  return FileResponse(
125
  path=full_path,
126
- filename=os.path.basename(full_path),
127
- media_type="video/mp4"
 
 
 
 
128
  )
129
 
130
  @app.get("/courses")
131
  async def list_courses():
132
  """List all unique course folder names based on extracted MP4 paths."""
 
 
133
  course_folders = set()
134
 
135
- print(f"[DEBUG] Scanning {MP4_OUTPUT_FOLDER} for courses")
136
 
137
  if os.path.exists(MP4_OUTPUT_FOLDER):
138
  for root, dirs, files in os.walk(MP4_OUTPUT_FOLDER):
@@ -142,29 +214,33 @@ async def list_courses():
142
  course_folder = os.path.dirname(rel_path)
143
  if course_folder and course_folder != ".":
144
  course_folders.add(course_folder)
145
- print(f"[DEBUG] Found course: {course_folder} (from file: {rel_path})")
146
 
147
  courses_list = sorted(course_folders)
148
- print(f"[DEBUG] Returning courses: {courses_list}")
149
  return JSONResponse(content={"courses": courses_list, "total": len(courses_list)})
150
 
151
  @app.get("/images/{course_folder:path}")
152
  async def list_mp4s_in_course(course_folder: str):
153
  """List MP4s available for a specific course folder"""
154
- print(f"[DEBUG] Listing MP4s for course: {course_folder}")
 
 
155
 
156
  course_path = os.path.join(MP4_OUTPUT_FOLDER, course_folder)
157
- print(f"[DEBUG] Course path: {course_path}")
158
- print(f"[DEBUG] Path exists: {os.path.exists(course_path)}")
159
 
160
  if not os.path.exists(course_path):
 
 
161
  # List what's actually available
162
- print(f"[DEBUG] Course folder not found. Available folders:")
163
  if os.path.exists(MP4_OUTPUT_FOLDER):
164
  for item in os.listdir(MP4_OUTPUT_FOLDER):
165
  item_path = os.path.join(MP4_OUTPUT_FOLDER, item)
166
  if os.path.isdir(item_path):
167
- print(f"[DEBUG] {item}")
168
 
169
  raise HTTPException(status_code=404, detail=f"Course folder not found: {course_folder}")
170
 
@@ -173,19 +249,24 @@ async def list_mp4s_in_course(course_folder: str):
173
  for file in os.listdir(course_path):
174
  if file.lower().endswith(".mp4"):
175
  mp4s.append(file)
176
- print(f"[DEBUG] Found MP4: {file}")
 
 
177
  except Exception as e:
178
- print(f"[ERROR] Failed to list files in {course_path}: {e}")
179
  raise HTTPException(status_code=500, detail="Failed to list files")
180
 
181
- print(f"[DEBUG] Returning MP4s: {mp4s}")
182
  return JSONResponse(content=mp4s)
183
 
184
- # Add debug endpoint to help troubleshoot
185
  @app.get("/debug/structure")
186
  async def debug_file_structure():
187
  """Debug endpoint to show the actual file structure"""
 
 
188
  structure = {}
 
 
189
 
190
  if os.path.exists(MP4_OUTPUT_FOLDER):
191
  for root, dirs, files in os.walk(MP4_OUTPUT_FOLDER):
@@ -193,15 +274,39 @@ async def debug_file_structure():
193
  if rel_root == ".":
194
  rel_root = "/"
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  structure[rel_root] = {
197
  "directories": dirs,
198
- "mp4_files": [f for f in files if f.lower().endswith('.mp4')],
199
- "other_files": [f for f in files if not f.lower().endswith('.mp4')]
200
  }
201
 
 
 
202
  return JSONResponse(content={
203
  "mp4_output_folder": MP4_OUTPUT_FOLDER,
204
  "folder_exists": os.path.exists(MP4_OUTPUT_FOLDER),
 
 
205
  "structure": structure
206
  })
207
 
@@ -209,8 +314,10 @@ async def debug_file_structure():
209
  async def startup_event():
210
  """Run the processing loop in the background when the API starts"""
211
  global processing_thread
 
 
212
  if not (processing_thread and processing_thread.is_alive()):
213
- # log_message("🚀 Starting RAR extraction, frame extraction, and vision analysis pipeline in background...")
214
  processing_thread = threading.Thread(target=process_hf_files_background)
215
  processing_thread.daemon = True
216
  processing_thread.start()
 
2
  from fastapi.responses import FileResponse, JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  import os
5
+ import shutil
6
  import uuid
7
  import time
8
  from typing import Dict, List, Optional
9
  import threading
10
+ import logging
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
 
16
  # Import processing functions and variables
17
  from processing_logic import (
 
30
  # Configure CORS
31
  app.add_middleware(
32
  CORSMiddleware,
33
+ allow_origins=["*"],
34
  allow_credentials=True,
35
+ allow_methods=["*"],
36
+ allow_headers=["*"],
37
  )
38
 
39
  # Define MP4_UPLOAD_FOLDER if not imported from processing_logic
40
+ MP4_UPLOAD_FOLDER = os.path.join(UPLOAD_DIRECTORY, "uploads") if 'UPLOAD_DIRECTORY' in globals() else "uploads"
41
+ os.makedirs(MP4_UPLOAD_FOLDER, exist_ok=True)
42
+ os.makedirs(MP4_OUTPUT_FOLDER, exist_ok=True)
43
 
44
  processing_thread = None
45
 
 
50
  with open(save_path, "wb") as f:
51
  shutil.copyfileobj(uploaded_file.file, f)
52
 
53
+ def log_request(endpoint: str, params: dict = None):
54
+ """Log API requests for debugging"""
55
+ logger.info(f"API Request: {endpoint} - Params: {params}")
56
+
57
  # === ROUTES ===
58
 
59
  @app.get("/")
 
62
  return {
63
  "message": "MP4 Processing API",
64
  "version": "1.0.0",
65
+ "status": "running",
66
  "endpoints": {
67
  "upload": "POST /upload - Upload MP4 file",
68
+ "download": "GET /download?course={course}&file={file} - Download MP4 file",
69
  "list": "GET /list - List uploaded files",
70
  "courses": "GET /courses - List all course folders",
71
  "images": "GET /images/{course_folder:path} - List MP4s in course",
72
+ "debug": "GET /debug/structure - Debug file structure"
 
 
73
  }
74
  }
75
 
76
  @app.post("/upload/")
77
  async def upload_file(file: UploadFile = File(...)):
78
  """Uploads a file and stores it by filename inside the upload folder"""
79
+ log_request("upload", {"filename": file.filename})
80
+
81
+ if not file.filename.lower().endswith('.mp4'):
82
+ raise HTTPException(status_code=400, detail="Only MP4 files are allowed")
83
+
84
  save_path = os.path.join(MP4_UPLOAD_FOLDER, file.filename)
85
  save_file(file, save_path)
86
+
87
+ logger.info(f"File uploaded successfully: {file.filename}")
88
+ return {"status": "uploaded", "filename": file.filename, "size": os.path.getsize(save_path)}
89
 
90
  @app.get("/list")
91
  async def list_uploaded_files():
92
  """List uploaded MP4 files"""
93
+ log_request("list")
94
+
95
  files = []
96
  if os.path.exists(MP4_UPLOAD_FOLDER):
97
  for file in os.listdir(MP4_UPLOAD_FOLDER):
98
  if file.lower().endswith(".mp4"):
99
+ file_path = os.path.join(MP4_UPLOAD_FOLDER, file)
100
+ files.append({
101
+ "filename": file,
102
+ "size": os.path.getsize(file_path),
103
+ "modified": os.path.getmtime(file_path)
104
+ })
105
+
106
+ logger.info(f"Listed {len(files)} uploaded files")
107
  return JSONResponse(content=files)
108
 
109
+ @app.get("/download")
110
+ async def download_file(course: str = Query(..., description="Course folder name"),
111
+ file: str = Query(..., description="MP4 filename")):
112
  """
113
  Download any MP4 under processed_mp4s, including nested course folders.
114
+ Usage: GET /download?course=MDS_CGAdventure_DownloadPirate.com&file=1.mp4
115
  """
116
+ log_request("download", {"course": course, "file": file})
117
+
118
+ logger.info(f"[DOWNLOAD] Request for course: {course}, file: {file}")
119
 
120
+ # Validate inputs
121
+ if not course or not file:
122
+ raise HTTPException(status_code=400, detail="Both course and file parameters are required")
123
+
124
+ if not file.lower().endswith('.mp4'):
125
+ raise HTTPException(status_code=400, detail="File must be an MP4")
126
+
127
+ # Security: Clean the inputs
128
+ course = course.strip().replace('..', '').replace('\\', '/')
129
+ file = file.strip().replace('..', '').replace('\\', '/')
130
+
131
+ # Prevent path traversal in individual components
132
+ if '/' in file or '\\' in file:
133
+ raise HTTPException(status_code=400, detail="File parameter cannot contain path separators")
134
+
135
+ # Construct the path
136
+ mp4_path = f"{course}/{file}"
137
+ full_path = os.path.join(MP4_OUTPUT_FOLDER, course, file)
138
+
139
+ logger.info(f"[DOWNLOAD] Constructed path: {mp4_path}")
140
+ logger.info(f"[DOWNLOAD] Full system path: {full_path}")
141
 
142
+ # Security: Prevent directory traversal
143
  full_path = os.path.normpath(full_path)
144
+ base_path = os.path.abspath(MP4_OUTPUT_FOLDER)
145
+
146
+ if not full_path.startswith(base_path):
147
+ logger.warning(f"[SECURITY] Path traversal attempt - course: {course}, file: {file}")
148
+ raise HTTPException(status_code=400, detail="Invalid path - security violation")
149
 
150
+ logger.info(f"[DOWNLOAD] Normalized path: {full_path}")
151
+ logger.info(f"[DOWNLOAD] File exists: {os.path.exists(full_path)}")
 
152
 
153
  if not os.path.exists(full_path):
154
+ logger.warning(f"[DOWNLOAD] File not found: {full_path}")
 
155
 
156
+ # Debug: Show what's actually in the course folder
157
+ course_path = os.path.join(MP4_OUTPUT_FOLDER, course)
158
+ logger.info(f"[DOWNLOAD] Checking course folder: {course_path}")
 
 
 
 
 
159
 
160
+ if os.path.exists(course_path):
161
+ logger.info(f"[DOWNLOAD] Course folder exists, contents:")
162
+ try:
163
+ for item in os.listdir(course_path):
164
+ item_path = os.path.join(course_path, item)
165
+ if os.path.isfile(item_path):
166
+ logger.info(f"[DOWNLOAD] File: {item} ({os.path.getsize(item_path)} bytes)")
167
+ else:
168
+ logger.info(f"[DOWNLOAD] Directory: {item}")
169
+ except Exception as e:
170
+ logger.error(f"[DOWNLOAD] Error listing course contents: {e}")
171
+ else:
172
+ logger.info(f"[DOWNLOAD] Course folder does not exist")
173
+
174
+ # Show available courses
175
+ logger.info("[DOWNLOAD] Available courses:")
176
+ if os.path.exists(MP4_OUTPUT_FOLDER):
177
+ for item in os.listdir(MP4_OUTPUT_FOLDER):
178
+ item_path = os.path.join(MP4_OUTPUT_FOLDER, item)
179
+ if os.path.isdir(item_path):
180
+ logger.info(f"[DOWNLOAD] Course: {item}")
181
+
182
+ raise HTTPException(
183
+ status_code=404,
184
+ detail=f"File not found: {file} in course {course}"
185
+ )
186
 
187
+ file_size = os.path.getsize(full_path)
188
+ logger.info(f"[DOWNLOAD] Serving file: {full_path} ({file_size} bytes)")
189
+
190
  return FileResponse(
191
  path=full_path,
192
+ filename=file,
193
+ media_type="video/mp4",
194
+ headers={
195
+ "Content-Length": str(file_size),
196
+ "Accept-Ranges": "bytes"
197
+ }
198
  )
199
 
200
  @app.get("/courses")
201
  async def list_courses():
202
  """List all unique course folder names based on extracted MP4 paths."""
203
+ log_request("courses")
204
+
205
  course_folders = set()
206
 
207
+ logger.info(f"[COURSES] Scanning {MP4_OUTPUT_FOLDER} for courses")
208
 
209
  if os.path.exists(MP4_OUTPUT_FOLDER):
210
  for root, dirs, files in os.walk(MP4_OUTPUT_FOLDER):
 
214
  course_folder = os.path.dirname(rel_path)
215
  if course_folder and course_folder != ".":
216
  course_folders.add(course_folder)
217
+ logger.info(f"[COURSES] Found course: {course_folder} (from file: {rel_path})")
218
 
219
  courses_list = sorted(course_folders)
220
+ logger.info(f"[COURSES] Returning {len(courses_list)} courses: {courses_list}")
221
  return JSONResponse(content={"courses": courses_list, "total": len(courses_list)})
222
 
223
  @app.get("/images/{course_folder:path}")
224
  async def list_mp4s_in_course(course_folder: str):
225
  """List MP4s available for a specific course folder"""
226
+ log_request("images", {"course_folder": course_folder})
227
+
228
+ logger.info(f"[IMAGES] Listing MP4s for course: {course_folder}")
229
 
230
  course_path = os.path.join(MP4_OUTPUT_FOLDER, course_folder)
231
+ logger.info(f"[IMAGES] Course path: {course_path}")
232
+ logger.info(f"[IMAGES] Path exists: {os.path.exists(course_path)}")
233
 
234
  if not os.path.exists(course_path):
235
+ logger.warning(f"[IMAGES] Course folder not found: {course_folder}")
236
+
237
  # List what's actually available
238
+ logger.info("[IMAGES] Available course folders:")
239
  if os.path.exists(MP4_OUTPUT_FOLDER):
240
  for item in os.listdir(MP4_OUTPUT_FOLDER):
241
  item_path = os.path.join(MP4_OUTPUT_FOLDER, item)
242
  if os.path.isdir(item_path):
243
+ logger.info(f"[IMAGES] {item}")
244
 
245
  raise HTTPException(status_code=404, detail=f"Course folder not found: {course_folder}")
246
 
 
249
  for file in os.listdir(course_path):
250
  if file.lower().endswith(".mp4"):
251
  mp4s.append(file)
252
+ file_path = os.path.join(course_path, file)
253
+ file_size = os.path.getsize(file_path)
254
+ logger.info(f"[IMAGES] Found MP4: {file} ({file_size} bytes)")
255
  except Exception as e:
256
+ logger.error(f"[IMAGES] Failed to list files in {course_path}: {e}")
257
  raise HTTPException(status_code=500, detail="Failed to list files")
258
 
259
+ logger.info(f"[IMAGES] Returning {len(mp4s)} MP4s: {mp4s}")
260
  return JSONResponse(content=mp4s)
261
 
 
262
  @app.get("/debug/structure")
263
  async def debug_file_structure():
264
  """Debug endpoint to show the actual file structure"""
265
+ log_request("debug/structure")
266
+
267
  structure = {}
268
+ total_files = 0
269
+ total_size = 0
270
 
271
  if os.path.exists(MP4_OUTPUT_FOLDER):
272
  for root, dirs, files in os.walk(MP4_OUTPUT_FOLDER):
 
274
  if rel_root == ".":
275
  rel_root = "/"
276
 
277
+ mp4_files = []
278
+ other_files = []
279
+
280
+ for file in files:
281
+ file_path = os.path.join(root, file)
282
+ file_size = os.path.getsize(file_path)
283
+ total_size += file_size
284
+
285
+ file_info = {
286
+ "name": file,
287
+ "size": file_size,
288
+ "modified": os.path.getmtime(file_path)
289
+ }
290
+
291
+ if file.lower().endswith('.mp4'):
292
+ mp4_files.append(file_info)
293
+ total_files += 1
294
+ else:
295
+ other_files.append(file_info)
296
+
297
  structure[rel_root] = {
298
  "directories": dirs,
299
+ "mp4_files": mp4_files,
300
+ "other_files": other_files
301
  }
302
 
303
+ logger.info(f"[DEBUG] File structure scan complete - {total_files} MP4 files, {total_size} bytes total")
304
+
305
  return JSONResponse(content={
306
  "mp4_output_folder": MP4_OUTPUT_FOLDER,
307
  "folder_exists": os.path.exists(MP4_OUTPUT_FOLDER),
308
+ "total_mp4_files": total_files,
309
+ "total_size_bytes": total_size,
310
  "structure": structure
311
  })
312
 
 
314
  async def startup_event():
315
  """Run the processing loop in the background when the API starts"""
316
  global processing_thread
317
+ logger.info("Starting up MP4 Processing API...")
318
+
319
  if not (processing_thread and processing_thread.is_alive()):
320
+ logger.info("🚀 Starting background processing thread...")
321
  processing_thread = threading.Thread(target=process_hf_files_background)
322
  processing_thread.daemon = True
323
  processing_thread.start()