diff --git a/web.py b/web.py
index a8d2651..4491d5f 100644
--- a/web.py
+++ b/web.py
@@ -17,6 +17,7 @@ import json
import math
import os
import re
+import shutil
import struct
import wave
from datetime import datetime
@@ -68,6 +69,21 @@ def _get_wav_info(path: Path):
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
+ if SOUNDFILE_AVAILABLE and ext in ('.flac', '.ogg', '.opus'):
+ try:
+ with sf.SoundFile(path) as f:
+ return round(len(f) / f.samplerate, 2)
+ except Exception:
+ return None
+ return None
+
+
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."""
@@ -209,9 +225,7 @@ def list_files(recordings_dir: str):
stat = path.stat()
rel = str(path.relative_to(base)).replace('\\', '/')
- duration = None
- if path.suffix.lower() == '.wav':
- duration, _, _ = _get_wav_info(path)
+ duration = _get_audio_duration(path)
files.append({
'name': rel,
@@ -248,6 +262,10 @@ class _Handler(BaseHTTPRequestHandler):
self._api_analyze(qs)
elif p == '/api/status':
self._api_status()
+ elif p == '/api/storage':
+ self._api_storage()
+ elif p == '/api/config':
+ self._api_config()
elif p.startswith('/download/'):
self._download(unquote(p[len('/download/'):]))
elif p.startswith('/stream/'):
@@ -272,6 +290,12 @@ class _Handler(BaseHTTPRequestHandler):
if path is None:
return
+ try:
+ threshold = float(qs.get('threshold', [self.threshold])[0])
+ threshold = max(0.0, min(1.0, threshold))
+ except (ValueError, TypeError):
+ threshold = self.threshold
+
status_path = Path(self.recordings_dir) / 'status.json'
try:
with open(status_path) as fh:
@@ -283,12 +307,12 @@ class _Handler(BaseHTTPRequestHandler):
ext = path.suffix.lower()
if ext == '.wav':
- result = analyze_wav(path, threshold=self.threshold)
+ result = analyze_wav(path, threshold=threshold)
elif ext == '.flac':
if not (NUMPY_AVAILABLE and SOUNDFILE_AVAILABLE):
self._json_err(400, 'FLAC analysis requires: pip install numpy soundfile')
return
- result = analyze_flac(path, threshold=self.threshold)
+ result = analyze_flac(path, threshold=threshold)
else:
self._json_err(400, f'Loudness analysis is not available for {ext} files')
return
@@ -374,6 +398,27 @@ class _Handler(BaseHTTPRequestHandler):
break
self.wfile.write(chunk)
+ def _api_storage(self):
+ base = Path(self.recordings_dir)
+ used = 0
+ if base.exists():
+ used = sum(
+ p.stat().st_size
+ for p in base.rglob('*')
+ if p.is_file() and p.suffix.lower() in AUDIO_EXTENSIONS
+ )
+ try:
+ du = shutil.disk_usage(str(base) if base.exists() else '.')
+ disk_free, disk_total = du.free, du.total
+ except Exception:
+ disk_free = disk_total = None
+ data = json.dumps({'used': used, 'disk_free': disk_free, 'disk_total': disk_total})
+ self._send(200, data.encode(), 'application/json')
+
+ def _api_config(self):
+ data = json.dumps({'threshold': self.threshold})
+ self._send(200, data.encode(), 'application/json')
+
def _safe_path(self, filename: str):
base = Path(self.recordings_dir).resolve()
try:
@@ -425,10 +470,18 @@ body{background:var(--bg);color:var(--txt);font:14px/1.5 system-ui,sans-serif}
/* sr-only */
.sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
-header{padding:20px 28px;border-bottom:1px solid var(--brd);
- display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
+header{padding:16px 28px;border-bottom:1px solid var(--brd);
+ display:flex;align-items:center;gap:12px;flex-wrap:wrap}
header h1{font-size:18px;font-weight:600}
-#subtitle{color:var(--muted);font-size:13px;margin-right:auto}
+#subtitle{color:var(--muted);font-size:13px}
+#storage-info{color:var(--muted);font-size:12px;margin-right:auto}
+.controls-bar{display:flex;align-items:center;gap:10px;padding:10px 28px;
+ border-bottom:1px solid var(--brd);background:var(--surf);flex-wrap:wrap}
+.controls-bar label{font-size:13px;color:var(--muted);white-space:nowrap}
+.controls-bar input[type=number]{width:70px;background:var(--bg);border:1px solid var(--brd);
+ color:var(--txt);padding:3px 6px;border-radius:4px;font-size:13px}
+.controls-bar input[type=number]:focus{outline:2px solid var(--accent);outline-offset:1px}
+.controls-hint{font-size:11px;color:var(--muted)}
.wrap{padding:20px 28px}
table{width:100%;border-collapse:collapse}
th{text-align:left;padding:9px 10px;color:var(--muted);font-weight:500;font-size:12px;
@@ -479,8 +532,15 @@ audio{width:100%;height:36px;border-radius:4px;display:block;
ISR Archive
Loading…
+