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:
2026-06-10 11:42:45 +02:00
parent 792f2b1fd5
commit b9089f9c18
+39 -61
View File
@@ -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