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 tempfile
|
||||||
import wave
|
import wave
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
||||||
@@ -62,21 +62,15 @@ MIME_TYPES = {
|
|||||||
# Audio analysis helpers
|
# 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):
|
def _get_audio_duration(path: Path):
|
||||||
"""Return duration in seconds for any supported audio file, or None."""
|
"""Return duration in seconds for any supported audio file, or None."""
|
||||||
ext = path.suffix.lower()
|
ext = path.suffix.lower()
|
||||||
if ext == '.wav':
|
if ext == '.wav':
|
||||||
dur, _, _ = _get_wav_info(path)
|
try:
|
||||||
return dur
|
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'):
|
if SOUNDFILE_AVAILABLE and ext in ('.flac', '.ogg', '.opus'):
|
||||||
try:
|
try:
|
||||||
with sf.SoundFile(path) as f:
|
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,
|
def _compute_rms_windows_wav(wf, channels: int, sampwidth: int, framerate: int,
|
||||||
window_samples: int):
|
window_samples: int):
|
||||||
"""Yield rms_0_to_1 for every window in the open wave file."""
|
"""Yield rms_0_to_1 for every window in the open wave file."""
|
||||||
frame_pos = 0
|
|
||||||
while True:
|
while True:
|
||||||
raw = wf.readframes(window_samples)
|
raw = wf.readframes(window_samples)
|
||||||
if not raw:
|
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
|
rms = math.sqrt(sum(s * s for s in mono) / len(mono)) / 32768.0
|
||||||
|
|
||||||
yield round(rms, 5)
|
yield round(rms, 5)
|
||||||
frame_pos += window_samples
|
|
||||||
|
|
||||||
|
|
||||||
def _loud_sections(rms_values: list, window_dur: float, duration: float,
|
def _loud_sections(rms_values: list, window_dur: float, duration: float,
|
||||||
@@ -365,14 +357,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
min_gap = self.min_gap
|
min_gap = self.min_gap
|
||||||
|
|
||||||
status_path = Path(self.recordings_dir) / 'status.json'
|
if self._is_active(filename):
|
||||||
try:
|
self._json_err(409, 'File is currently being recorded — analysis unavailable until recording stops')
|
||||||
with open(status_path) as fh:
|
return
|
||||||
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
|
|
||||||
|
|
||||||
recordings_base = Path(self.recordings_dir).resolve()
|
recordings_base = Path(self.recordings_dir).resolve()
|
||||||
analyses_base = Path(self.analyses_dir).resolve()
|
analyses_base = Path(self.analyses_dir).resolve()
|
||||||
@@ -431,11 +418,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
with open(path, 'rb') as fh:
|
with open(path, 'rb') as fh:
|
||||||
while True:
|
self._copy_to_response(fh)
|
||||||
chunk = fh.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
self.wfile.write(chunk)
|
|
||||||
|
|
||||||
def _stream(self, filename: str):
|
def _stream(self, filename: str):
|
||||||
"""Serve audio for inline playback with HTTP Range support."""
|
"""Serve audio for inline playback with HTTP Range support."""
|
||||||
@@ -466,13 +449,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
with open(path, 'rb') as fh:
|
with open(path, 'rb') as fh:
|
||||||
fh.seek(start)
|
fh.seek(start)
|
||||||
remaining = length
|
self._copy_to_response(fh, length)
|
||||||
while remaining > 0:
|
|
||||||
chunk = fh.read(min(65536, remaining))
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
self.wfile.write(chunk)
|
|
||||||
remaining -= len(chunk)
|
|
||||||
else:
|
else:
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', content_type)
|
self.send_header('Content-Type', content_type)
|
||||||
@@ -481,11 +458,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
with open(path, 'rb') as fh:
|
with open(path, 'rb') as fh:
|
||||||
while True:
|
self._copy_to_response(fh)
|
||||||
chunk = fh.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
self.wfile.write(chunk)
|
|
||||||
|
|
||||||
def _api_storage(self):
|
def _api_storage(self):
|
||||||
base = Path(self.recordings_dir)
|
base = Path(self.recordings_dir)
|
||||||
@@ -509,14 +482,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
self._send(200, data.encode(), 'application/json')
|
self._send(200, data.encode(), 'application/json')
|
||||||
|
|
||||||
def _api_delete(self, filename: str):
|
def _api_delete(self, filename: str):
|
||||||
status_path = Path(self.recordings_dir) / 'status.json'
|
if self._is_active(filename):
|
||||||
try:
|
self._json_err(409, 'Cannot delete a file that is currently being recorded')
|
||||||
with open(status_path) as fh:
|
return
|
||||||
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
|
|
||||||
|
|
||||||
path = self._safe_path(filename)
|
path = self._safe_path(filename)
|
||||||
if path is None:
|
if path is None:
|
||||||
@@ -563,14 +531,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
if path is None:
|
if path is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
status_path = Path(self.recordings_dir) / 'status.json'
|
if self._is_active(filename):
|
||||||
try:
|
self._json_err(409, 'Cannot cut a file that is currently being recorded')
|
||||||
with open(status_path) as fh:
|
return
|
||||||
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 not shutil.which('ffmpeg'):
|
if not shutil.which('ffmpeg'):
|
||||||
self._json_err(500, 'ffmpeg is not available on this server')
|
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.send_header('Content-Length', str(tmp_size))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
with open(tmp_path, 'rb') as fh:
|
with open(tmp_path, 'rb') as fh:
|
||||||
while True:
|
self._copy_to_response(fh)
|
||||||
chunk = fh.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
self.wfile.write(chunk)
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
self._json_err(504, 'ffmpeg timed out — file may be too large')
|
self._json_err(504, 'ffmpeg timed out — file may be too large')
|
||||||
finally:
|
finally:
|
||||||
@@ -619,12 +578,31 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def _safe_path(self, filename: str):
|
||||||
base = Path(self.recordings_dir).resolve()
|
base = Path(self.recordings_dir).resolve()
|
||||||
try:
|
try:
|
||||||
path = (base / filename).resolve()
|
path = (base / filename).resolve()
|
||||||
path.relative_to(base)
|
path.relative_to(base)
|
||||||
except (ValueError, Exception):
|
except Exception:
|
||||||
self._send(403, b'Forbidden', 'text/plain')
|
self._send(403, b'Forbidden', 'text/plain')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user