refactor: deduplicate web.py server code
- Extract _is_active() helper for the three duplicated status.json active-recording checks (analyze, delete, cut) - Extract _copy_to_response() for the four duplicated 64 KB chunk copy loops (download, stream full/range, cut) - Inline _get_wav_info into _get_audio_duration (sample rate and channels were never used) - Remove unused HTTPServer import, dead frame_pos counter, and the redundant (ValueError, Exception) catch in _safe_path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import subprocess
|
||||
import tempfile
|
||||
import wave
|
||||
from datetime import datetime
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
@@ -62,21 +62,15 @@ MIME_TYPES = {
|
||||
# Audio analysis helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_wav_info(path: Path):
|
||||
"""Return (duration_seconds, sample_rate, channels) or (None, None, None)."""
|
||||
try:
|
||||
with wave.open(str(path), 'rb') as wf:
|
||||
return wf.getnframes() / wf.getframerate(), wf.getframerate(), wf.getnchannels()
|
||||
except Exception:
|
||||
return None, None, None
|
||||
|
||||
|
||||
def _get_audio_duration(path: Path):
|
||||
"""Return duration in seconds for any supported audio file, or None."""
|
||||
ext = path.suffix.lower()
|
||||
if ext == '.wav':
|
||||
dur, _, _ = _get_wav_info(path)
|
||||
return dur
|
||||
try:
|
||||
with wave.open(str(path), 'rb') as wf:
|
||||
return wf.getnframes() / wf.getframerate()
|
||||
except Exception:
|
||||
return None
|
||||
if SOUNDFILE_AVAILABLE and ext in ('.flac', '.ogg', '.opus'):
|
||||
try:
|
||||
with sf.SoundFile(path) as f:
|
||||
@@ -89,7 +83,6 @@ def _get_audio_duration(path: Path):
|
||||
def _compute_rms_windows_wav(wf, channels: int, sampwidth: int, framerate: int,
|
||||
window_samples: int):
|
||||
"""Yield rms_0_to_1 for every window in the open wave file."""
|
||||
frame_pos = 0
|
||||
while True:
|
||||
raw = wf.readframes(window_samples)
|
||||
if not raw:
|
||||
@@ -110,7 +103,6 @@ def _compute_rms_windows_wav(wf, channels: int, sampwidth: int, framerate: int,
|
||||
rms = math.sqrt(sum(s * s for s in mono) / len(mono)) / 32768.0
|
||||
|
||||
yield round(rms, 5)
|
||||
frame_pos += window_samples
|
||||
|
||||
|
||||
def _loud_sections(rms_values: list, window_dur: float, duration: float,
|
||||
@@ -365,14 +357,9 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
except (ValueError, TypeError):
|
||||
min_gap = self.min_gap
|
||||
|
||||
status_path = Path(self.recordings_dir) / 'status.json'
|
||||
try:
|
||||
with open(status_path) as fh:
|
||||
if filename in set(json.load(fh).get('active', [])):
|
||||
self._json_err(409, 'File is currently being recorded — analysis unavailable until recording stops')
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if self._is_active(filename):
|
||||
self._json_err(409, 'File is currently being recorded — analysis unavailable until recording stops')
|
||||
return
|
||||
|
||||
recordings_base = Path(self.recordings_dir).resolve()
|
||||
analyses_base = Path(self.analyses_dir).resolve()
|
||||
@@ -431,11 +418,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
with open(path, 'rb') as fh:
|
||||
while True:
|
||||
chunk = fh.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
self._copy_to_response(fh)
|
||||
|
||||
def _stream(self, filename: str):
|
||||
"""Serve audio for inline playback with HTTP Range support."""
|
||||
@@ -466,13 +449,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
|
||||
with open(path, 'rb') as fh:
|
||||
fh.seek(start)
|
||||
remaining = length
|
||||
while remaining > 0:
|
||||
chunk = fh.read(min(65536, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
self._copy_to_response(fh, length)
|
||||
else:
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', content_type)
|
||||
@@ -481,11 +458,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
with open(path, 'rb') as fh:
|
||||
while True:
|
||||
chunk = fh.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
self._copy_to_response(fh)
|
||||
|
||||
def _api_storage(self):
|
||||
base = Path(self.recordings_dir)
|
||||
@@ -509,14 +482,9 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
self._send(200, data.encode(), 'application/json')
|
||||
|
||||
def _api_delete(self, filename: str):
|
||||
status_path = Path(self.recordings_dir) / 'status.json'
|
||||
try:
|
||||
with open(status_path) as fh:
|
||||
if filename in set(json.load(fh).get('active', [])):
|
||||
self._json_err(409, 'Cannot delete a file that is currently being recorded')
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if self._is_active(filename):
|
||||
self._json_err(409, 'Cannot delete a file that is currently being recorded')
|
||||
return
|
||||
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
@@ -563,14 +531,9 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
if path is None:
|
||||
return
|
||||
|
||||
status_path = Path(self.recordings_dir) / 'status.json'
|
||||
try:
|
||||
with open(status_path) as fh:
|
||||
if filename in set(json.load(fh).get('active', [])):
|
||||
self._json_err(409, 'Cannot cut a file that is currently being recorded')
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if self._is_active(filename):
|
||||
self._json_err(409, 'Cannot cut a file that is currently being recorded')
|
||||
return
|
||||
|
||||
if not shutil.which('ffmpeg'):
|
||||
self._json_err(500, 'ffmpeg is not available on this server')
|
||||
@@ -606,11 +569,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
self.send_header('Content-Length', str(tmp_size))
|
||||
self.end_headers()
|
||||
with open(tmp_path, 'rb') as fh:
|
||||
while True:
|
||||
chunk = fh.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
self._copy_to_response(fh)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._json_err(504, 'ffmpeg timed out — file may be too large')
|
||||
finally:
|
||||
@@ -619,12 +578,31 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _is_active(self, filename: str) -> bool:
|
||||
"""True if isr.py reports this file as currently being recorded."""
|
||||
try:
|
||||
with open(Path(self.recordings_dir) / 'status.json') as fh:
|
||||
return filename in json.load(fh).get('active', [])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _copy_to_response(self, fh, length=None):
|
||||
"""Stream an open binary file to the client in 64 KB chunks."""
|
||||
remaining = length
|
||||
while remaining is None or remaining > 0:
|
||||
chunk = fh.read(65536 if remaining is None else min(65536, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
if remaining is not None:
|
||||
remaining -= len(chunk)
|
||||
|
||||
def _safe_path(self, filename: str):
|
||||
base = Path(self.recordings_dir).resolve()
|
||||
try:
|
||||
path = (base / filename).resolve()
|
||||
path.relative_to(base)
|
||||
except (ValueError, Exception):
|
||||
except Exception:
|
||||
self._send(403, b'Forbidden', 'text/plain')
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user