Add web upload: POST /api/upload stores MP4s into dated archive, plus Upload UI panel

This commit is contained in:
2026-08-28 14:30:18 +01:00
parent 1be5e840af
commit 35be3fc2e7
2 changed files with 106 additions and 1 deletions
+58 -1
View File
@@ -109,7 +109,10 @@ class PlayerHandler(BaseHTTPRequestHandler):
self.send_error(404)
def do_POST(self):
if urlparse(self.path).path == "/api/encode":
path = urlparse(self.path).path
if path == "/api/upload":
return self.handle_upload()
if path == "/api/encode":
if not self.server.encode_status["running"]:
threading.Thread(target=run_encoding, args=(self.server,), daemon=True).start()
return self.send_json(self.server.encode_status)
@@ -140,6 +143,60 @@ class PlayerHandler(BaseHTTPRequestHandler):
return self.send_json(highlight)
self.send_error(404)
def handle_upload(self):
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
return self.send_error(400, "Invalid content length")
max_bytes = 2 * 1024 * 1024 * 1024
if not 0 < length <= max_bytes:
return self.send_error(413, "Missing or oversized body (limit 2 GiB)")
filename = parse_qs(urlparse(self.path).query).get("filename", [""])[0]
match = VIDEO_RE.fullmatch(filename)
if not match:
return self.send_error(400, "Filename must be HH.MM.SS.mp4 or HH.MM.SS.avi (e.g. 09.28.36.mp4)")
hour, minute, second = map(int, match.groups()[:3])
if hour > 23 or minute > 59 or second > 59:
return self.send_error(400, "Invalid time in filename")
try:
day = normalize_date(parse_qs(urlparse(self.path).query).get("date", [""])[0])
except ValueError:
return self.send_error(400, "date must be YYYYMMDD or YYYY-MM-DD")
suffix = match.group(4).lower()
if suffix == "avi":
return self.send_error(400, "Only pre-encoded .mp4 uploads are accepted; AVI sources go through the Encoding menu")
target_dir = self.server.output_root / day
target = target_dir / f"{hour:02d}.{minute:02d}.{second:02d}.mp4"
temporary = target.with_suffix(".uploading.mp4")
try:
target_dir.mkdir(parents=True, exist_ok=True)
received = 0
with temporary.open("wb") as sink:
while received < length:
chunk = self.rfile.read(min(256 * 1024, length - received))
if not chunk:
raise OSError("client disconnected mid-upload")
sink.write(chunk)
received += len(chunk)
if target.exists():
temporary.unlink()
return self.send_error(409, "A recording already exists at that time; nothing was overwritten")
temporary.replace(target)
except OSError:
temporary.unlink(missing_ok=True)
return self.send_error(500, "Failed to store upload")
self.server.recordings.setdefault(day, [])
if not any(clip["name"] == target.name for clip in self.server.recordings[day]):
self.server.recordings[day].append({
"name": target.name,
"start": hour * 3600 + minute * 60 + second,
"duration": 120,
"path": target,
})
self.server.recordings[day].sort(key=lambda clip: clip["start"])
return self.send_json({"ok": True, "file": f"{day}/{target.name}"})
def do_DELETE(self):
request = urlparse(self.path)
if request.path != "/api/highlights":